From e5efc68a9101d7d7e38263c8a6ee44dda991fc6a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 17:34:48 +0100 Subject: [PATCH 01/31] Done macro --- Cargo.lock | 8 ++++ Cargo.toml | 21 +++++--- rustlings-macros/Cargo.toml | 12 +++++ rustlings-macros/src/lib.rs | 95 +++++++++++++++++++++++++++++++++++++ src/main.rs | 23 +++++++++ 5 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 rustlings-macros/Cargo.toml create mode 100644 rustlings-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f4853d0c..e432072d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,6 +574,7 @@ dependencies = [ "indicatif", "notify-debouncer-mini", "predicates", + "rustlings-macros", "serde", "serde_json", "shlex", @@ -582,6 +583,13 @@ dependencies = [ "winnow", ] +[[package]] +name = "rustlings-macros" +version = "5.6.1" +dependencies = [ + "quote", +] + [[package]] name = "ryu" version = "1.0.17" diff --git a/Cargo.toml b/Cargo.toml index 2d152cfc..e08be8bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,30 @@ -[package] -name = "rustlings" -description = "Small exercises to get you used to reading and writing Rust code!" +[workspace] +resolver = "2" + +[workspace.package] version = "5.6.1" authors = [ "Liv ", "Carol (Nichols || Goulding) ", ] +license = "MIT" edition = "2021" +[package] +name = "rustlings" +description = "Small exercises to get you used to reading and writing Rust code!" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + [dependencies] anyhow = "1.0.81" clap = { version = "4.5.4", features = ["derive"] } console = "0.15.8" indicatif = "0.17.8" notify-debouncer-mini = "0.4.1" +rustlings-macros = { path = "rustlings-macros" } serde_json = "1.0.115" serde = { version = "1.0.197", features = ["derive"] } shlex = "1.3.0" @@ -21,10 +32,6 @@ toml_edit = { version = "0.22.9", default-features = false, features = ["parse", which = "6.0.1" winnow = "0.6.5" -[[bin]] -name = "rustlings" -path = "src/main.rs" - [dev-dependencies] assert_cmd = "2.0.14" glob = "0.3.0" diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml new file mode 100644 index 00000000..0114c8f0 --- /dev/null +++ b/rustlings-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rustlings-macros" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.35" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs new file mode 100644 index 00000000..dd1a5881 --- /dev/null +++ b/rustlings-macros/src/lib.rs @@ -0,0 +1,95 @@ +use proc_macro::TokenStream; +use quote::quote; +use std::{fs::read_dir, panic, path::PathBuf}; + +fn path_to_string(path: PathBuf) -> String { + path.into_os_string() + .into_string() + .unwrap_or_else(|original| { + panic!("The path {} is invalid UTF8", original.to_string_lossy()); + }) +} + +#[proc_macro] +pub fn include_files(_: TokenStream) -> TokenStream { + let mut files = Vec::with_capacity(8); + let mut dirs = Vec::with_capacity(128); + + for entry in read_dir("exercises").expect("Failed to open the exercises directory") { + let entry = entry.expect("Failed to read the exercises directory"); + + if entry.file_type().unwrap().is_file() { + let path = entry.path(); + if path.file_name().unwrap() != "README.md" { + files.push(path_to_string(path)); + } + + continue; + } + + let dir_path = entry.path(); + let dir_files = read_dir(&dir_path).unwrap_or_else(|e| { + panic!("Failed to open the directory {}: {e}", dir_path.display()); + }); + let dir_path = path_to_string(dir_path); + let dir_files = dir_files.filter_map(|entry| { + let entry = entry.unwrap_or_else(|e| { + panic!("Failed to read the directory {dir_path}: {e}"); + }); + let path = entry.path(); + + if !entry.file_type().unwrap().is_file() { + panic!("Found {} but expected only files", path.display()); + } + + if path.file_name().unwrap() == "README.md" { + return None; + } + + if path.extension() != Some("rs".as_ref()) { + panic!( + "Found {} but expected only README.md and .rs files", + path.display(), + ); + } + + Some(path_to_string(path)) + }); + + dirs.push(quote! { + EmbeddedFlatDir { + path: #dir_path, + readme: EmbeddedFile { + path: concat!(#dir_path, "/README.md"), + content: ::std::include_bytes!(concat!("../", #dir_path, "/README.md")), + }, + content: vec![ + #(EmbeddedFile { + path: #dir_files, + content: ::std::include_bytes!(concat!("../", #dir_files)), + }),* + ], + } + }); + } + + quote! { + EmbeddedFiles { + info_toml_content: ::std::include_str!("../info.toml"), + exercises_dir: ExercisesDir { + readme: EmbeddedFile { + path: "exercises/README.md", + content: ::std::include_bytes!("../exercises/README.md"), + }, + files: vec![#( + EmbeddedFile { + path: #files, + content: ::std::include_bytes!(concat!("../", #files)), + } + ),*], + dirs: vec![#(#dirs),*], + }, + } + } + .into() +} diff --git a/src/main.rs b/src/main.rs index 8f73dbba..fed8c117 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,28 @@ mod project; mod run; mod verify; +struct EmbeddedFile { + path: &'static str, + content: &'static [u8], +} + +struct EmbeddedFlatDir { + path: &'static str, + readme: EmbeddedFile, + content: Vec, +} + +struct ExercisesDir { + readme: EmbeddedFile, + files: Vec, + dirs: Vec, +} + +struct EmbeddedFiles { + info_toml_content: &'static str, + exercises_dir: ExercisesDir, +} + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -87,6 +109,7 @@ enum Subcommands { } fn main() -> Result<()> { + let embedded_files = rustlings_macros::include_files!(); let args = Args::parse(); if args.command.is_none() { From dd025391f2f3a4cb0a45e28163b01538b4b525cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 17:52:51 +0100 Subject: [PATCH 02/31] Make everything static --- rustlings-macros/src/lib.rs | 6 +++--- src/main.rs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index dd1a5881..d8cd05ce 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -63,7 +63,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { path: concat!(#dir_path, "/README.md"), content: ::std::include_bytes!(concat!("../", #dir_path, "/README.md")), }, - content: vec![ + content: &[ #(EmbeddedFile { path: #dir_files, content: ::std::include_bytes!(concat!("../", #dir_files)), @@ -81,13 +81,13 @@ pub fn include_files(_: TokenStream) -> TokenStream { path: "exercises/README.md", content: ::std::include_bytes!("../exercises/README.md"), }, - files: vec![#( + files: &[#( EmbeddedFile { path: #files, content: ::std::include_bytes!(concat!("../", #files)), } ),*], - dirs: vec![#(#dirs),*], + dirs: &[#(#dirs),*], }, } } diff --git a/src/main.rs b/src/main.rs index fed8c117..7822d122 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,13 +35,13 @@ struct EmbeddedFile { struct EmbeddedFlatDir { path: &'static str, readme: EmbeddedFile, - content: Vec, + content: &'static [EmbeddedFile], } struct ExercisesDir { readme: EmbeddedFile, - files: Vec, - dirs: Vec, + files: &'static [EmbeddedFile], + dirs: &'static [EmbeddedFlatDir], } struct EmbeddedFiles { @@ -49,6 +49,8 @@ struct EmbeddedFiles { exercises_dir: ExercisesDir, } +static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -109,7 +111,6 @@ enum Subcommands { } fn main() -> Result<()> { - let embedded_files = rustlings_macros::include_files!(); let args = Args::parse(); if args.command.is_none() { From 39bdd086a775d87115691b830f65e2a438874fec Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 18:18:20 +0100 Subject: [PATCH 03/31] Use concat explicitly from std --- rustlings-macros/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index d8cd05ce..598b5c35 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -60,13 +60,13 @@ pub fn include_files(_: TokenStream) -> TokenStream { EmbeddedFlatDir { path: #dir_path, readme: EmbeddedFile { - path: concat!(#dir_path, "/README.md"), - content: ::std::include_bytes!(concat!("../", #dir_path, "/README.md")), + path: ::std::concat!(#dir_path, "/README.md"), + content: ::std::include_bytes!(::std::concat!("../", #dir_path, "/README.md")), }, content: &[ #(EmbeddedFile { path: #dir_files, - content: ::std::include_bytes!(concat!("../", #dir_files)), + content: ::std::include_bytes!(::std::concat!("../", #dir_files)), }),* ], } @@ -84,7 +84,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { files: &[#( EmbeddedFile { path: #files, - content: ::std::include_bytes!(concat!("../", #files)), + content: ::std::include_bytes!(::std::concat!("../", #files)), } ),*], dirs: &[#(#dirs),*], From d5ed749e9fde03212fd6fe5d60e2ddfe9b2429c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 21:06:36 +0100 Subject: [PATCH 04/31] Add embedded.rs --- src/embedded.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 25 +----------- 2 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 src/embedded.rs diff --git a/src/embedded.rs b/src/embedded.rs new file mode 100644 index 00000000..8f6c14e7 --- /dev/null +++ b/src/embedded.rs @@ -0,0 +1,101 @@ +use std::{ + fs::{create_dir, File, OpenOptions}, + io::{self, Write}, + path::Path, +}; + +pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); + +#[derive(Clone, Copy)] +pub enum WriteStrategy { + IfNotExists, + Overwrite, +} + +impl WriteStrategy { + fn open>(self, path: P) -> io::Result { + match self { + Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), + Self::Overwrite => OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path), + } + } +} + +struct EmbeddedFile { + path: &'static str, + content: &'static [u8], +} + +impl EmbeddedFile { + fn write_to_disk(&self, strategy: WriteStrategy) -> io::Result<()> { + strategy.open(self.path)?.write_all(self.content) + } +} + +struct EmbeddedFlatDir { + path: &'static str, + readme: EmbeddedFile, + content: &'static [EmbeddedFile], +} + +impl EmbeddedFlatDir { + fn init_on_disk(&self) -> io::Result<()> { + let path = Path::new(self.path); + + if let Err(e) = create_dir(path) { + if !path.is_dir() { + return Err(e); + } + } + + self.readme.write_to_disk(WriteStrategy::Overwrite) + } +} + +struct ExercisesDir { + readme: EmbeddedFile, + files: &'static [EmbeddedFile], + dirs: &'static [EmbeddedFlatDir], +} + +pub struct EmbeddedFiles { + info_toml_content: &'static str, + exercises_dir: ExercisesDir, +} + +impl EmbeddedFiles { + pub fn init_exercises_dir(&self) -> io::Result<()> { + create_dir("exercises")?; + self.exercises_dir + .readme + .write_to_disk(WriteStrategy::Overwrite) + } + + pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { + if let Some(file) = self + .exercises_dir + .files + .iter() + .find(|file| file.path == path.as_os_str()) + { + return file.write_to_disk(strategy); + } + + for dir in self.exercises_dir.dirs { + if let Some(file) = dir + .content + .iter() + .find(|file| file.path == path.as_os_str()) + { + dir.init_on_disk()?; + return file.write_to_disk(strategy); + } + } + + Err(io::Error::from(io::ErrorKind::NotFound)) + } +} diff --git a/src/main.rs b/src/main.rs index 7822d122..1e0aa668 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,35 +22,12 @@ use std::time::Duration; #[macro_use] mod ui; +mod embedded; mod exercise; mod project; mod run; mod verify; -struct EmbeddedFile { - path: &'static str, - content: &'static [u8], -} - -struct EmbeddedFlatDir { - path: &'static str, - readme: EmbeddedFile, - content: &'static [EmbeddedFile], -} - -struct ExercisesDir { - readme: EmbeddedFile, - files: &'static [EmbeddedFile], - dirs: &'static [EmbeddedFlatDir], -} - -struct EmbeddedFiles { - info_toml_content: &'static str, - exercises_dir: ExercisesDir, -} - -static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); - /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] From 5b4103bbac180fcb1de747214647811a3622b476 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 21:10:31 +0100 Subject: [PATCH 05/31] Remove unneeded ./ from relative paths --- src/exercise.rs | 4 ++-- src/main.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 19f528a8..16e4a41c 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -13,7 +13,7 @@ const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; const CONTEXT: usize = 2; -const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml"; +const CLIPPY_CARGO_TOML_PATH: &str = "exercises/22_clippy/Cargo.toml"; // Checks if the line contains the "I AM NOT DONE" comment. fn contains_not_done_comment(input: &str) -> bool { @@ -36,7 +36,7 @@ fn temp_file() -> String { .filter(|c| c.is_alphanumeric()) .collect(); - format!("./temp_{}_{thread_id}", process::id()) + format!("temp_{}_{thread_id}", process::id()) } // The mode of the exercise. diff --git a/src/main.rs b/src/main.rs index 1e0aa668..90d0109c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -342,7 +342,7 @@ fn watch( let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; debouncer .watcher() - .watch(Path::new("./exercises"), RecursiveMode::Recursive)?; + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; clear_screen(); From 3ff9b0cd2a92a531e8c7a9f8a0f86b9fac04d252 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 22:11:16 +0100 Subject: [PATCH 06/31] POC done --- src/embedded.rs | 23 ++++++++++++++--- src/exercise.rs | 2 +- src/main.rs | 67 ++++++++++++++++++++++++++++--------------------- src/project.rs | 20 +++++++-------- src/run.rs | 16 +++--------- 5 files changed, 74 insertions(+), 54 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 8f6c14e7..25dbe641 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -52,7 +52,9 @@ impl EmbeddedFlatDir { } } - self.readme.write_to_disk(WriteStrategy::Overwrite) + self.readme.write_to_disk(WriteStrategy::Overwrite)?; + + Ok(()) } } @@ -63,16 +65,31 @@ struct ExercisesDir { } pub struct EmbeddedFiles { - info_toml_content: &'static str, + pub info_toml_content: &'static str, exercises_dir: ExercisesDir, } impl EmbeddedFiles { pub fn init_exercises_dir(&self) -> io::Result<()> { create_dir("exercises")?; + self.exercises_dir .readme - .write_to_disk(WriteStrategy::Overwrite) + .write_to_disk(WriteStrategy::IfNotExists)?; + + for file in self.exercises_dir.files { + file.write_to_disk(WriteStrategy::IfNotExists)?; + } + + for dir in self.exercises_dir.dirs { + dir.init_on_disk()?; + + for file in dir.content { + file.write_to_disk(WriteStrategy::IfNotExists)?; + } + } + + Ok(()) } pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { diff --git a/src/exercise.rs b/src/exercise.rs index 16e4a41c..7c2e5fde 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -36,7 +36,7 @@ fn temp_file() -> String { .filter(|c| c.is_alphanumeric()) .collect(); - format!("temp_{}_{thread_id}", process::id()) + format!("./temp_{}_{thread_id}", process::id()) } // The mode of the exercise. diff --git a/src/main.rs b/src/main.rs index 90d0109c..822cd1ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,14 @@ use crate::verify::verify; use anyhow::Result; use clap::{Parser, Subcommand}; use console::Emoji; +use embedded::EMBEDDED_FILES; use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; +use std::io::{self, prelude::*, stdin, stdout}; use std::path::Path; -use std::process::Command; +use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; @@ -54,7 +54,7 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// Reset a single exercise using "git stash -- " + /// Reset a single exercise Reset { /// The name of the exercise name: String, @@ -83,13 +83,45 @@ enum Subcommands { #[arg(short, long)] solved: bool, }, - /// Enable rust-analyzer for exercises - Lsp, } fn main() -> Result<()> { let args = Args::parse(); + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + .unwrap() + .exercises; + + if !Path::new("exercises").is_dir() { + let mut stdout = stdout().lock(); + write!( + stdout, + "The `exercises` directory wasn't found in the current directory. +Do you want to initialize Rustlings in the current directory (y/n)? " + )?; + stdout.flush()?; + let mut answer = String::new(); + stdin().read_line(&mut answer)?; + answer.make_ascii_lowercase(); + if answer.trim() != "y" { + exit(1); + } + + EMBEDDED_FILES.init_exercises_dir()?; + if let Err(e) = write_project_json(&exercises) { + writeln!( + stdout, + "Failed to write rust-project.json to disk for rust-analyzer: {e}" + )?; + } else { + writeln!(stdout, "Successfully generated rust-project.json")?; + writeln!( + stdout, + "rust-analyzer will now parse exercises, restart your language server or editor" + )?; + } + } + if args.command.is_none() { println!("\n{WELCOME}\n"); } @@ -101,18 +133,6 @@ fn main() -> Result<()> { std::process::exit(1); } - let info_file = fs::read_to_string("info.toml").unwrap_or_else(|e| { - match e.kind() { - io::ErrorKind::NotFound => println!( - "The program must be run from the rustlings directory\nTry `cd rustlings/`!", - ), - _ => println!("Failed to read the info.toml file: {e}"), - } - std::process::exit(1); - }); - let exercises = toml_edit::de::from_str::(&info_file) - .unwrap() - .exercises; let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { @@ -205,7 +225,7 @@ fn main() -> Result<()> { Subcommands::Reset { name } => { let exercise = find_exercise(&name, &exercises); - reset(exercise).unwrap_or_else(|_| std::process::exit(1)); + reset(exercise)?; } Subcommands::Hint { name } => { @@ -219,15 +239,6 @@ fn main() -> Result<()> { .unwrap_or_else(|_| std::process::exit(1)); } - Subcommands::Lsp => { - if let Err(e) = write_project_json(exercises) { - println!("Failed to write rust-project.json to disk for rust-analyzer: {e}"); - } else { - println!("Successfully generated rust-project.json"); - println!("rust-analyzer will now parse exercises, restart your language server or editor"); - } - } - Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Err(e) => { println!("Error: Could not watch your progress. Error message was {e:?}."); diff --git a/src/project.rs b/src/project.rs index 0f56de96..bb6caa58 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use serde::Serialize; use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use crate::exercise::Exercise; @@ -9,14 +9,14 @@ use crate::exercise::Exercise; /// Contains the structure of resulting rust-project.json file /// and functions to build the data required to create the file #[derive(Serialize)] -struct RustAnalyzerProject { +struct RustAnalyzerProject<'a> { sysroot_src: PathBuf, - crates: Vec, + crates: Vec>, } #[derive(Serialize)] -struct Crate { - root_module: PathBuf, +struct Crate<'a> { + root_module: &'a Path, edition: &'static str, // Not used, but required in the JSON file. deps: Vec<()>, @@ -25,12 +25,12 @@ struct Crate { cfg: [&'static str; 1], } -impl RustAnalyzerProject { - fn build(exercises: Vec) -> Result { +impl<'a> RustAnalyzerProject<'a> { + fn build(exercises: &'a [Exercise]) -> Result { let crates = exercises - .into_iter() + .iter() .map(|exercise| Crate { - root_module: exercise.path, + root_module: &exercise.path, edition: "2021", deps: Vec::new(), // This allows rust_analyzer to work inside `#[test]` blocks @@ -69,7 +69,7 @@ impl RustAnalyzerProject { } /// Write `rust-project.json` to disk. -pub fn write_project_json(exercises: Vec) -> Result<()> { +pub fn write_project_json(exercises: &[Exercise]) -> Result<()> { let content = RustAnalyzerProject::build(exercises)?; // Using the capacity 2^14 since the file length in bytes is higher than 2^13. diff --git a/src/run.rs b/src/run.rs index 6dd0388f..792bd8fd 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,6 +1,7 @@ -use std::process::Command; +use std::io; use std::time::Duration; +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, Mode}; use crate::verify::test; use indicatif::ProgressBar; @@ -19,17 +20,8 @@ pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { } // Resets the exercise by stashing the changes. -pub fn reset(exercise: &Exercise) -> Result<(), ()> { - let command = Command::new("git") - .arg("stash") - .arg("--") - .arg(&exercise.path) - .spawn(); - - match command { - Ok(_) => Ok(()), - Err(_) => Err(()), - } +pub fn reset(exercise: &Exercise) -> io::Result<()> { + EMBEDDED_FILES.write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) } // Invoke the rust compiler on the path of the given exercise From 3959570221c88bf7bebbc7427236ae5c90d9d630 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:25:21 +0100 Subject: [PATCH 07/31] Bump version to v6 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e432072d..d8e5b723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,7 +564,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "5.6.1" +version = "6.0.0" dependencies = [ "anyhow", "assert_cmd", @@ -585,7 +585,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "5.6.1" +version = "6.0.0" dependencies = [ "quote", ] diff --git a/Cargo.toml b/Cargo.toml index e08be8bf..690aecc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" [workspace.package] -version = "5.6.1" +version = "6.0.0" authors = [ "Liv ", "Carol (Nichols || Goulding) ", From 0f18d599e92189d5f3ba085dcb4c8c4da9c7f584 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:25:32 +0100 Subject: [PATCH 08/31] Add panic = "abort" --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 690aecc4..9224364b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,9 @@ winnow = "0.6.5" assert_cmd = "2.0.14" glob = "0.3.0" predicates = "3.1.0" + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" From 36a8e3ac0ee4f59ed587725e3257a79129a981e2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:29:41 +0100 Subject: [PATCH 09/31] Replace rust-project.json with Cargo.toml --- src/init.rs | 75 ++++++++++++++++++++++++++++++++++ src/main.rs | 108 +++++++++++++++++++++---------------------------- src/project.rs | 83 ------------------------------------- 3 files changed, 122 insertions(+), 144 deletions(-) create mode 100644 src/init.rs delete mode 100644 src/project.rs diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 00000000..66535354 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,75 @@ +use anyhow::{bail, Context, Result}; +use std::{ + env::set_current_dir, + fs::{create_dir, OpenOptions}, + io::{self, ErrorKind, Write}, + path::Path, +}; + +use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; + +fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { + let mut cargo_toml = Vec::with_capacity(1 << 13); + cargo_toml.extend_from_slice( + br#"[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false +"#, + ); + for exercise in exercises { + cargo_toml.extend_from_slice(b"\n[[bin]]\nname = \""); + cargo_toml.extend_from_slice(exercise.name.as_bytes()); + cargo_toml.extend_from_slice(b"\"\npath = \""); + cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); + cargo_toml.extend_from_slice(b"\"\n"); + } + OpenOptions::new() + .create_new(true) + .write(true) + .open("Cargo.toml")? + .write_all(&cargo_toml) +} + +fn create_vscode_dir() -> Result<()> { + create_dir(".vscode").context("Failed to create the directory `.vscode`")?; + let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; + OpenOptions::new() + .create_new(true) + .write(true) + .open(".vscode/extensions.json")? + .write_all(vs_code_extensions_json)?; + + Ok(()) +} + +pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> { + let rustlings_path = Path::new("rustlings"); + if let Err(e) = create_dir(rustlings_path) { + if e.kind() == ErrorKind::AlreadyExists { + bail!( + "A directory with the name `rustligs` already exists in the current directory. +You probably already initialized Rustlings. +Run `cd rustlings` +Then run `rustlings` again" + ); + } + return Err(e.into()); + } + + set_current_dir("rustlings") + .context("Failed to change the current directory to `rustlings`")?; + + EMBEDDED_FILES + .init_exercises_dir() + .context("Failed to initialize the `rustlings/exercises` directory")?; + + create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + + create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + + println!("\nDone initialization!\n"); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 822cd1ad..36c36b54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ use crate::exercise::{Exercise, ExerciseList}; -use crate::project::write_project_json; use crate::run::{reset, run}; use crate::verify::verify; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; use embedded::EMBEDDED_FILES; @@ -10,7 +9,7 @@ use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::io::{self, prelude::*, stdin, stdout}; +use std::io::{self, prelude::*}; use std::path::Path; use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -24,7 +23,7 @@ mod ui; mod embedded; mod exercise; -mod project; +mod init; mod run; mod verify; @@ -41,6 +40,8 @@ struct Args { #[derive(Subcommand)] enum Subcommands { + /// Initialize Rustlings + Init, /// Verify all exercises according to the recommended order Verify, /// Rerun `verify` when files were edited @@ -88,40 +89,6 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - .unwrap() - .exercises; - - if !Path::new("exercises").is_dir() { - let mut stdout = stdout().lock(); - write!( - stdout, - "The `exercises` directory wasn't found in the current directory. -Do you want to initialize Rustlings in the current directory (y/n)? " - )?; - stdout.flush()?; - let mut answer = String::new(); - stdin().read_line(&mut answer)?; - answer.make_ascii_lowercase(); - if answer.trim() != "y" { - exit(1); - } - - EMBEDDED_FILES.init_exercises_dir()?; - if let Err(e) = write_project_json(&exercises) { - writeln!( - stdout, - "Failed to write rust-project.json to disk for rust-analyzer: {e}" - )?; - } else { - writeln!(stdout, "Successfully generated rust-project.json")?; - writeln!( - stdout, - "rust-analyzer will now parse exercises, restart your language server or editor" - )?; - } - } - if args.command.is_none() { println!("\n{WELCOME}\n"); } @@ -133,14 +100,32 @@ Do you want to initialize Rustlings in the current directory (y/n)? " std::process::exit(1); } - let verbose = args.nocapture; + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + .unwrap() + .exercises; + if matches!(args.command, Some(Subcommands::Init)) { + init::init_rustlings(&exercises).context("Initialization failed")?; + println!("{DEFAULT_OUT}\n"); + return Ok(()); + } else if !Path::new("exercises").is_dir() { + println!( + "\nThe `exercises` directory wasn't found in the current directory. +If you are just starting with Rustlings and want to initialize it, +run the command `rustlings init`" + ); + exit(1); + } + + let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); std::process::exit(0); }); match command { + // `Init` is handled above. + Subcommands::Init => (), Subcommands::List { paths, names, @@ -421,9 +406,16 @@ fn watch( } } -const DEFAULT_OUT: &str = "Thanks for installing Rustlings! +const WELCOME: &str = r" welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/"; -Is this your first time? Don't worry, Rustlings was made for beginners! We are +const DEFAULT_OUT: &str = + "Is this your first time? Don't worry, Rustlings was made for beginners! We are going to teach you a lot of things about Rust, but before we can get started, here's a couple of notes about how Rustlings operates: @@ -446,8 +438,20 @@ started, here's a couple of notes about how Rustlings operates: 5. If you want to use `rust-analyzer` with exercises, which provides features like autocompletion, run the command `rustlings lsp`. -Got all that? Great! To get started, run `rustlings watch` in order to get the first -exercise. Make sure to have your editor open!"; +Got all that? Great! To get started, go into the new directory `rustlings` by +running `cd rustlings`. +Then, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the new `rustlings` directory!"; + +const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: + hint - prints the current exercise's hint + clear - clears the screen + quit - quits watch mode + ! - executes a command, like `!rustc --explain E0381` + help - displays this help message + +Watch mode automatically re-evaluates the current exercise +when you edit a file's contents."; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | @@ -475,21 +479,3 @@ You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; - -const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: - hint - prints the current exercise's hint - clear - clears the screen - quit - quits watch mode - ! - executes a command, like `!rustc --explain E0381` - help - displays this help message - -Watch mode automatically re-evaluates the current exercise -when you edit a file's contents."; diff --git a/src/project.rs b/src/project.rs deleted file mode 100644 index bb6caa58..00000000 --- a/src/project.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Serialize; -use std::env; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -use crate::exercise::Exercise; - -/// Contains the structure of resulting rust-project.json file -/// and functions to build the data required to create the file -#[derive(Serialize)] -struct RustAnalyzerProject<'a> { - sysroot_src: PathBuf, - crates: Vec>, -} - -#[derive(Serialize)] -struct Crate<'a> { - root_module: &'a Path, - edition: &'static str, - // Not used, but required in the JSON file. - deps: Vec<()>, - // Only `test` is used for all crates. - // Therefore, an array is used instead of a `Vec`. - cfg: [&'static str; 1], -} - -impl<'a> RustAnalyzerProject<'a> { - fn build(exercises: &'a [Exercise]) -> Result { - let crates = exercises - .iter() - .map(|exercise| Crate { - root_module: &exercise.path, - edition: "2021", - deps: Vec::new(), - // This allows rust_analyzer to work inside `#[test]` blocks - cfg: ["test"], - }) - .collect(); - - if let Some(path) = env::var_os("RUST_SRC_PATH") { - return Ok(Self { - sysroot_src: PathBuf::from(path), - crates, - }); - } - - let toolchain = Command::new("rustc") - .arg("--print") - .arg("sysroot") - .stderr(Stdio::inherit()) - .output() - .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? - .stdout; - - let toolchain = - String::from_utf8(toolchain).context("The toolchain path is invalid UTF8")?; - let toolchain = toolchain.trim_end(); - println!("Determined toolchain: {toolchain}\n"); - - let mut sysroot_src = PathBuf::with_capacity(256); - sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); - - Ok(Self { - sysroot_src, - crates, - }) - } -} - -/// Write `rust-project.json` to disk. -pub fn write_project_json(exercises: &[Exercise]) -> Result<()> { - let content = RustAnalyzerProject::build(exercises)?; - - // Using the capacity 2^14 since the file length in bytes is higher than 2^13. - // The final length is not known exactly because it depends on the user's sysroot path, - // the current number of exercises etc. - let mut buf = Vec::with_capacity(1 << 14); - serde_json::to_writer(&mut buf, &content)?; - std::fs::write("rust-project.json", buf)?; - - Ok(()) -} From a561a0f7f0378ac98ee4f025f5023c320af794b8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:51:08 +0100 Subject: [PATCH 10/31] Avoid reinitialization by mistake --- src/init.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/init.rs b/src/init.rs index 66535354..e640c258 100644 --- a/src/init.rs +++ b/src/init.rs @@ -45,6 +45,16 @@ fn create_vscode_dir() -> Result<()> { } pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> { + if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { + bail!( + "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist +in the current directory. It looks like Rustlings was already initialized here. +Run `rustlings` for instructions on getting started with the exercises. + +If you didn't already initialize Rustlings, please initialize it in another directory." + ); + } + let rustlings_path = Path::new("rustlings"); if let Err(e) = create_dir(rustlings_path) { if e.kind() == ErrorKind::AlreadyExists { From 2b01811fe9344fa4afdef95fb934745176cab1b2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:51:22 +0100 Subject: [PATCH 11/31] Fix typo --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index e640c258..1ec84847 100644 --- a/src/init.rs +++ b/src/init.rs @@ -59,7 +59,7 @@ If you didn't already initialize Rustlings, please initialize it in another dire if let Err(e) = create_dir(rustlings_path) { if e.kind() == ErrorKind::AlreadyExists { bail!( - "A directory with the name `rustligs` already exists in the current directory. + "A directory with the name `rustlings` already exists in the current directory. You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again" From 8e3cc9d70c627ace4553e4fe62af3443e970e64f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:52:05 +0100 Subject: [PATCH 12/31] Improve printed information --- src/init.rs | 2 -- src/main.rs | 17 ++++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/init.rs b/src/init.rs index 1ec84847..1edcb23c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -79,7 +79,5 @@ Then run `rustlings` again" create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - println!("\nDone initialization!\n"); - Ok(()) } diff --git a/src/main.rs b/src/main.rs index 36c36b54..76b63736 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,13 +106,16 @@ fn main() -> Result<()> { if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; - println!("{DEFAULT_OUT}\n"); + println!( + "\nDone initialization!\n +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` for further instructions on getting started." + ); return Ok(()); } else if !Path::new("exercises").is_dir() { println!( "\nThe `exercises` directory wasn't found in the current directory. -If you are just starting with Rustlings and want to initialize it, -run the command `rustlings init`" +If you are just starting with Rustlings, run the command `rustlings init` to initialize it." ); exit(1); } @@ -435,13 +438,9 @@ started, here's a couple of notes about how Rustlings operates: 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, and sometimes, other learners do too so you can help each other out! -5. If you want to use `rust-analyzer` with exercises, which provides features like - autocompletion, run the command `rustlings lsp`. -Got all that? Great! To get started, go into the new directory `rustlings` by -running `cd rustlings`. -Then, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the new `rustlings` directory!"; +Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the `rustlings` directory!"; const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: hint - prints the current exercise's hint From fe7d775818021acee7d5ae40c5cf9fdac69b2122 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 18:52:49 +0100 Subject: [PATCH 13/31] Remove the installation scripts --- install.ps1 | 94 --------------------------- install.sh | 184 ---------------------------------------------------- 2 files changed, 278 deletions(-) delete mode 100644 install.ps1 delete mode 100755 install.sh diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 844b0134..00000000 --- a/install.ps1 +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env pwsh - -#Requires -Version 5 -param($path = "$home/rustlings") - -Write-Host "Let's get you set up with Rustlings!" - -Write-Host "Checking requirements..." -if (Get-Command git -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Git is installed" -} else { - Write-Host "WARNING: Git does not seem to be installed." - Write-Host "Please download Git using your package manager or over https://git-scm.com/!" - exit 1 -} - -if (Get-Command rustc -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Rust is installed" -} else { - Write-Host "WARNING: Rust does not seem to be installed." - Write-Host "Please download Rust using https://rustup.rs!" - exit 1 -} - -if (Get-Command cargo -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Cargo is installed" -} else { - Write-Host "WARNING: Cargo does not seem to be installed." - Write-Host "Please download Rust and Cargo using https://rustup.rs!" - exit 1 -} - -# Function that compares two versions strings v1 and v2 given in arguments (e.g 1.31 and 1.33.0). -# Returns 1 if v1 > v2, 0 if v1 == v2, 2 if v1 < v2. -function vercomp($v1, $v2) { - if ($v1 -eq $v2) { - return 0 - } - - $v1 = $v1.Replace(".", "0") - $v2 = $v2.Replace(".", "0") - if ($v1.Length -gt $v2.Length) { - $v2 = $v2.PadRight($v1.Length, "0") - } else { - $v1 = $v1.PadRight($v2.Length, "0") - } - - if ($v1 -gt $v2) { - return 1 - } else { - return 2 - } -} - -$rustVersion = $(rustc --version).Split(" ")[1] -$minRustVersion = "1.70" -if ((vercomp $rustVersion $minRustVersion) -eq 2) { - Write-Host "WARNING: Rust version is too old: $rustVersion - needs at least $minRustVersion" - Write-Host "Please update Rust with 'rustup update'" - exit 1 -} else { - Write-Host "SUCCESS: Rust is up to date" -} - -Write-Host "Cloning Rustlings at $path" -git clone -q https://github.com/rust-lang/rustlings $path -if (!($LASTEXITCODE -eq 0)) { - exit 1 -} - -# UseBasicParsing is deprecated, pwsh 6 or above will automatically use it, -# but anyone running pwsh 5 will have to pass the argument. -$version = Invoke-WebRequest -UseBasicParsing https://api.github.com/repos/rust-lang/rustlings/releases/latest ` - | ConvertFrom-Json | Select-Object -ExpandProperty tag_name - -Write-Host "Checking out version $version..." -Set-Location $path -git checkout -q tags/$version - -Write-Host "Installing the 'rustlings' executable..." -cargo install --force --path . -if (!(Get-Command rustlings -ErrorAction SilentlyContinue)) { - Write-Host "WARNING: Please check that you have '~/.cargo/bin' in your PATH environment variable!" -} - -# Checking whether Clippy is installed. -# Due to a bug in Cargo, this must be done with Rustup: https://github.com/rust-lang/rustup/issues/1514 -$clippy = (rustup component list | Select-String "clippy" | Select-String "installed") | Out-String -if (!$clippy) { - Write-Host "Installing the 'cargo-clippy' executable..." - rustup component add clippy -} - -Write-Host "All done! Navigate to $path and run 'rustlings' to get started!" diff --git a/install.sh b/install.sh deleted file mode 100755 index fdbe8d43..00000000 --- a/install.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo -e "\nLet's get you set up with Rustlings!" - -echo "Checking requirements..." -if [ -x "$(command -v git)" ] -then - echo "SUCCESS: Git is installed" -else - echo "ERROR: Git does not seem to be installed." - echo "Please download Git using your package manager or over https://git-scm.com/!" - exit 1 -fi - -if [ -x "$(command -v cc)" ] -then - echo "SUCCESS: cc is installed" -else - echo "ERROR: cc does not seem to be installed." - echo "Please download (g)cc using your package manager." - echo "OSX: xcode-select --install" - echo "Deb: sudo apt install gcc" - echo "Yum: sudo yum -y install gcc" - exit 1 -fi - -if [ -x "$(command -v rustup)" ] -then - echo "SUCCESS: rustup is installed" -else - echo "ERROR: rustup does not seem to be installed." - echo "Please download rustup using https://rustup.rs!" - exit 1 -fi - -if [ -x "$(command -v rustc)" ] -then - echo "SUCCESS: Rust is installed" -else - echo "ERROR: Rust does not seem to be installed." - echo "Please download Rust using rustup!" - exit 1 -fi - -if [ -x "$(command -v cargo)" ] -then - echo "SUCCESS: Cargo is installed" -else - echo "ERROR: Cargo does not seem to be installed." - echo "Please download Rust and Cargo using rustup!" - exit 1 -fi - -# Look up python installations, starting with 3 with a fallback of 2 -if [ -x "$(command -v python3)" ] -then - PY="$(command -v python3)" -elif [ -x "$(command -v python)" ] -then - PY="$(command -v python)" -elif [ -x "$(command -v python2)" ] -then - PY="$(command -v python2)" -else - echo "ERROR: No working python installation was found" - echo "Please install python and add it to the PATH variable" - exit 1 -fi - -# Function that compares two versions strings v1 and v2 given in arguments (e.g 1.31 and 1.33.0). -# Returns 1 if v1 > v2, 0 if v1 == v2, 2 if v1 < v2. -function vercomp() { - if [[ $1 == $2 ]] - then - return 0 - fi - v1=( ${1//./ } ) - v2=( ${2//./ } ) - len1=${#v1[@]} - len2=${#v2[@]} - max_len=$len1 - if [[ $max_len -lt $len2 ]] - then - max_len=$len2 - fi - - #pad right in short arr - if [[ len1 -gt len2 ]]; - then - for ((i = len2; i < len1; i++)); - do - v2[$i]=0 - done - else - for ((i = len1; i < len2; i++)); - do - v1[$i]=0 - done - fi - - for i in `seq 0 $((max_len-1))` - do - # Fill empty fields with zeros in v1 - if [ -z "${v1[$i]}" ] - then - v1[$i]=0 - fi - # And in v2 - if [ -z "${v2[$i]}" ] - then - v2[$i]=0 - fi - if [ ${v1[$i]} -gt ${v2[$i]} ] - then - return 1 - fi - if [ ${v1[$i]} -lt ${v2[$i]} ] - then - return 2 - fi - done - return 0 -} - -RustVersion=$(rustc --version | cut -d " " -f 2) -MinRustVersion=1.70 -vercomp "$RustVersion" $MinRustVersion || ec=$? -if [ ${ec:-0} -eq 2 ] -then - echo "ERROR: Rust version is too old: $RustVersion - needs at least $MinRustVersion" - echo "Please update Rust with 'rustup update'" - exit 1 -else - echo "SUCCESS: Rust is up to date" -fi - -Path=${1:-rustlings/} -echo "Cloning Rustlings at $Path..." -git clone -q https://github.com/rust-lang/rustlings.git "$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']) if 'tag_name' in obj else sys.exit(f\"Error: {obj['message']}\");") -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 main branch" - Version="main" - else - Version="tags/${Version}" - fi -else - Version="tags/${Version}" -fi - -echo "Checking out version $Version..." -git checkout -q ${Version} - -echo "Installing the 'rustlings' executable..." -cargo install --force --path . - -if ! [ -x "$(command -v rustlings)" ] -then - echo "WARNING: Please check that you have '$CargoBin' in your PATH environment variable!" -fi - -# Checking whether Clippy is installed. -# Due to a bug in Cargo, this must be done with Rustup: https://github.com/rust-lang/rustup/issues/1514 -Clippy=$(rustup component list | grep "clippy" | grep "installed") -if [ -z "$Clippy" ] -then - echo "Installing the 'cargo-clippy' executable..." - rustup component add clippy -fi - -echo "All done! Run 'rustlings' to get started." From 79ca821e26711123c959e919eed2a630fa102cd5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 20:48:30 +0100 Subject: [PATCH 14/31] Fix tests --- src/exercise.rs | 10 +++++----- src/main.rs | 12 +++++++++--- tests/fixture/failure/{ => exercises}/compFailure.rs | 0 .../failure/{ => exercises}/compNoExercise.rs | 0 tests/fixture/failure/{ => exercises}/testFailure.rs | 0 .../fixture/failure/{ => exercises}/testNotPassed.rs | 0 tests/fixture/failure/info.toml | 4 ++-- .../state/{ => exercises}/finished_exercise.rs | 0 .../state/{ => exercises}/pending_exercise.rs | 0 .../state/{ => exercises}/pending_test_exercise.rs | 0 tests/fixture/state/info.toml | 7 +++---- tests/fixture/success/{ => exercises}/compSuccess.rs | 0 tests/fixture/success/{ => exercises}/testSuccess.rs | 0 tests/fixture/success/info.toml | 4 ++-- 14 files changed, 21 insertions(+), 16 deletions(-) rename tests/fixture/failure/{ => exercises}/compFailure.rs (100%) rename tests/fixture/failure/{ => exercises}/compNoExercise.rs (100%) rename tests/fixture/failure/{ => exercises}/testFailure.rs (100%) rename tests/fixture/failure/{ => exercises}/testNotPassed.rs (100%) rename tests/fixture/state/{ => exercises}/finished_exercise.rs (100%) rename tests/fixture/state/{ => exercises}/pending_exercise.rs (100%) rename tests/fixture/state/{ => exercises}/pending_test_exercise.rs (100%) rename tests/fixture/success/{ => exercises}/compSuccess.rs (100%) rename tests/fixture/success/{ => exercises}/testSuccess.rs (100%) diff --git a/src/exercise.rs b/src/exercise.rs index 7c2e5fde..11259168 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -354,7 +354,7 @@ mod test { File::create(temp_file()).unwrap(); let exercise = Exercise { name: String::from("example"), - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), mode: Mode::Compile, hint: String::from(""), }; @@ -372,7 +372,7 @@ mod test { let exercise = Exercise { name: String::from("example"), // We want a file that does actually compile - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), mode: *mode, hint: String::from(""), }; @@ -385,7 +385,7 @@ mod test { fn test_pending_state() { let exercise = Exercise { name: "pending_exercise".into(), - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), mode: Mode::Compile, hint: String::new(), }; @@ -426,7 +426,7 @@ mod test { fn test_finished_exercise() { let exercise = Exercise { name: "finished_exercise".into(), - path: PathBuf::from("tests/fixture/state/finished_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/finished_exercise.rs"), mode: Mode::Compile, hint: String::new(), }; @@ -438,7 +438,7 @@ mod test { fn test_exercise_with_output() { let exercise = Exercise { name: "exercise_with_output".into(), - path: PathBuf::from("tests/fixture/success/testSuccess.rs"), + path: PathBuf::from("tests/fixture/success/exercises/testSuccess.rs"), mode: Mode::Test, hint: String::new(), }; diff --git a/src/main.rs b/src/main.rs index 76b63736..2ac44d55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; +use std::fs; use std::io::{self, prelude::*}; use std::path::Path; use std::process::{exit, Command}; @@ -100,9 +101,14 @@ fn main() -> Result<()> { std::process::exit(1); } - let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - .unwrap() - .exercises; + // Read a local `info.toml` if it exists. Mainly to let the tests work for now. + let exercises = if let Ok(file_content) = fs::read_to_string("info.toml") { + toml_edit::de::from_str::(&file_content) + } else { + toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + } + .context("Failed to parse `info.toml`")? + .exercises; if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; diff --git a/tests/fixture/failure/compFailure.rs b/tests/fixture/failure/exercises/compFailure.rs similarity index 100% rename from tests/fixture/failure/compFailure.rs rename to tests/fixture/failure/exercises/compFailure.rs diff --git a/tests/fixture/failure/compNoExercise.rs b/tests/fixture/failure/exercises/compNoExercise.rs similarity index 100% rename from tests/fixture/failure/compNoExercise.rs rename to tests/fixture/failure/exercises/compNoExercise.rs diff --git a/tests/fixture/failure/testFailure.rs b/tests/fixture/failure/exercises/testFailure.rs similarity index 100% rename from tests/fixture/failure/testFailure.rs rename to tests/fixture/failure/exercises/testFailure.rs diff --git a/tests/fixture/failure/testNotPassed.rs b/tests/fixture/failure/exercises/testNotPassed.rs similarity index 100% rename from tests/fixture/failure/testNotPassed.rs rename to tests/fixture/failure/exercises/testNotPassed.rs diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml index e5949f9b..9474ee3f 100644 --- a/tests/fixture/failure/info.toml +++ b/tests/fixture/failure/info.toml @@ -1,11 +1,11 @@ [[exercises]] name = "compFailure" -path = "compFailure.rs" +path = "exercises/compFailure.rs" mode = "compile" hint = "" [[exercises]] name = "testFailure" -path = "testFailure.rs" +path = "exercises/testFailure.rs" mode = "test" hint = "Hello!" diff --git a/tests/fixture/state/finished_exercise.rs b/tests/fixture/state/exercises/finished_exercise.rs similarity index 100% rename from tests/fixture/state/finished_exercise.rs rename to tests/fixture/state/exercises/finished_exercise.rs diff --git a/tests/fixture/state/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs similarity index 100% rename from tests/fixture/state/pending_exercise.rs rename to tests/fixture/state/exercises/pending_exercise.rs diff --git a/tests/fixture/state/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs similarity index 100% rename from tests/fixture/state/pending_test_exercise.rs rename to tests/fixture/state/exercises/pending_test_exercise.rs diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml index 547b3a48..8de5d604 100644 --- a/tests/fixture/state/info.toml +++ b/tests/fixture/state/info.toml @@ -1,18 +1,17 @@ [[exercises]] name = "pending_exercise" -path = "pending_exercise.rs" +path = "exercises/pending_exercise.rs" mode = "compile" hint = """""" [[exercises]] name = "pending_test_exercise" -path = "pending_test_exercise.rs" +path = "exercises/pending_test_exercise.rs" mode = "test" hint = """""" [[exercises]] name = "finished_exercise" -path = "finished_exercise.rs" +path = "exercises/finished_exercise.rs" mode = "compile" hint = """""" - diff --git a/tests/fixture/success/compSuccess.rs b/tests/fixture/success/exercises/compSuccess.rs similarity index 100% rename from tests/fixture/success/compSuccess.rs rename to tests/fixture/success/exercises/compSuccess.rs diff --git a/tests/fixture/success/testSuccess.rs b/tests/fixture/success/exercises/testSuccess.rs similarity index 100% rename from tests/fixture/success/testSuccess.rs rename to tests/fixture/success/exercises/testSuccess.rs diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml index 68d35388..17ed8c62 100644 --- a/tests/fixture/success/info.toml +++ b/tests/fixture/success/info.toml @@ -1,11 +1,11 @@ [[exercises]] name = "compSuccess" -path = "compSuccess.rs" +path = "exercises/compSuccess.rs" mode = "compile" hint = """""" [[exercises]] name = "testSuccess" -path = "testSuccess.rs" +path = "exercises/testSuccess.rs" mode = "test" hint = """""" From 23f0fae1c8eddfa1ac679d8167ec63b554c554b9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 21:13:28 +0100 Subject: [PATCH 15/31] Show a success message after resetting --- src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2ac44d55..1926f6ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -212,19 +212,17 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Subcommands::Run { name } => { let exercise = find_exercise(&name, &exercises); - run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); } Subcommands::Reset { name } => { let exercise = find_exercise(&name, &exercises); - reset(exercise)?; + println!("The file {} has been reset!", exercise.path.display()); } Subcommands::Hint { name } => { let exercise = find_exercise(&name, &exercises); - println!("{}", exercise.hint); } From b5e17c965d1fee01336fdfabd93c575555a44d62 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 21:15:11 +0100 Subject: [PATCH 16/31] Add an error message when a file is not embedded --- src/embedded.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/embedded.rs b/src/embedded.rs index 25dbe641..f65b8aef 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -113,6 +113,9 @@ impl EmbeddedFiles { } } - Err(io::Error::from(io::ErrorKind::NotFound)) + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("{} not found in the embedded files", path.display()), + )) } } From 1e1f0317134fc3588f2eea4a118bd72aba3f9b34 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 00:49:19 +0100 Subject: [PATCH 17/31] Fix path comparison --- src/embedded.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index f65b8aef..56b4b618 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -97,17 +97,13 @@ impl EmbeddedFiles { .exercises_dir .files .iter() - .find(|file| file.path == path.as_os_str()) + .find(|file| Path::new(file.path) == path) { return file.write_to_disk(strategy); } for dir in self.exercises_dir.dirs { - if let Some(file) = dir - .content - .iter() - .find(|file| file.path == path.as_os_str()) - { + if let Some(file) = dir.content.iter().find(|file| Path::new(file.path) == path) { dir.init_on_disk()?; return file.write_to_disk(strategy); } From b711dd692afaf42830efb04c491616d3f069fbdf Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 02:04:41 +0100 Subject: [PATCH 18/31] Add .gitignore --- src/init.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/init.rs b/src/init.rs index 1edcb23c..d958c96e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -32,6 +32,15 @@ publish = false .write_all(&cargo_toml) } +fn create_gitignore() -> io::Result<()> { + let gitignore = b"/target"; + OpenOptions::new() + .create_new(true) + .write(true) + .open(".gitignore")? + .write_all(gitignore) +} + fn create_vscode_dir() -> Result<()> { create_dir(".vscode").context("Failed to create the directory `.vscode`")?; let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; @@ -77,6 +86,8 @@ Then run `rustlings` again" create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; + create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; Ok(()) From 82b563f1654860ba3590d91ec3c0f321e3130ae2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 16:55:33 +0200 Subject: [PATCH 19/31] Use Cargo instead of rustc --- src/exercise.rs | 262 +++++++++++------------------------------------- src/main.rs | 25 ++--- src/run.rs | 53 +++------- src/verify.rs | 119 +++++++++------------- 4 files changed, 132 insertions(+), 327 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 11259168..83d444fc 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,21 +1,21 @@ +use anyhow::{Context, Result}; use serde::Deserialize; -use std::fmt::{self, Display, Formatter}; -use std::fs::{self, remove_file, File}; +use std::fmt::{self, Debug, Display, Formatter}; +use std::fs::{self, File}; use std::io::{self, BufRead, BufReader}; use std::path::PathBuf; -use std::process::{self, exit, Command, Stdio}; -use std::{array, env, mem}; +use std::process::{exit, Command, Output}; +use std::{array, mem}; use winnow::ascii::{space0, Caseless}; use winnow::combinator::opt; use winnow::Parser; -const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; -const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; -const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; -const CONTEXT: usize = 2; -const CLIPPY_CARGO_TOML_PATH: &str = "exercises/22_clippy/Cargo.toml"; +use crate::embedded::EMBEDDED_FILES; -// Checks if the line contains the "I AM NOT DONE" comment. +// The number of context lines above and below a highlighted line. +const CONTEXT: usize = 2; + +// Check if the line contains the "I AM NOT DONE" comment. fn contains_not_done_comment(input: &str) -> bool { ( space0::<_, ()>, @@ -28,26 +28,15 @@ fn contains_not_done_comment(input: &str) -> bool { .is_ok() } -// Get a temporary file name that is hopefully unique -#[inline] -fn temp_file() -> String { - let thread_id: String = format!("{:?}", std::thread::current().id()) - .chars() - .filter(|c| c.is_alphanumeric()) - .collect(); - - format!("./temp_{}_{thread_id}", process::id()) -} - // The mode of the exercise. -#[derive(Deserialize, Copy, Clone, Debug)] +#[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum Mode { - // Indicates that the exercise should be compiled as a binary + // The exercise should be compiled as a binary Compile, - // Indicates that the exercise should be compiled as a test harness + // The exercise should be compiled as a test harness Test, - // Indicates that the exercise should be linted with clippy + // The exercise should be linted with clippy Clippy, } @@ -56,171 +45,72 @@ pub struct ExerciseList { pub exercises: Vec, } -// A representation of a rustlings exercise. -// This is deserialized from the accompanying info.toml file -#[derive(Deserialize, Debug)] +impl ExerciseList { + pub fn parse() -> Result { + // Read a local `info.toml` if it exists. + // Mainly to let the tests work for now. + if let Ok(file_content) = fs::read_to_string("info.toml") { + toml_edit::de::from_str(&file_content) + } else { + toml_edit::de::from_str(EMBEDDED_FILES.info_toml_content) + } + .context("Failed to parse `info.toml`") + } +} + +// Deserialized from the `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) + // The mode of the exercise 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 +// The state of an Exercise. #[derive(PartialEq, Eq, 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 +// The context information of a pending exercise. #[derive(PartialEq, Eq, Debug)] pub struct ContextLine { - // The source code that is still pending completion + // The source code line pub line: String, - // The line number of the source code still pending completion + // The line number pub number: usize, - // Whether or not this is important + // Whether this is important and should be highlighted 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, -} - -struct FileHandle; - -impl Drop for FileHandle { - fn drop(&mut self) { - clean(); - } -} - impl Exercise { - pub fn compile(&self) -> Result { - let cmd = match self.mode { - Mode::Compile => Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Test => Command::new("rustc") - .args(["--test", self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Clippy => { - let cargo_toml = format!( - r#"[package] -name = "{}" -version = "0.0.1" -edition = "2021" -[[bin]] -name = "{}" -path = "{}.rs""#, - self.name, self.name, self.name - ); - let cargo_toml_error_msg = if env::var("NO_EMOJI").is_ok() { - "Failed to write Clippy Cargo.toml file." - } else { - "Failed to write 📎 Clippy 📎 Cargo.toml file." - }; - fs::write(CLIPPY_CARGO_TOML_PATH, cargo_toml).expect(cargo_toml_error_msg); - // To support the ability to run the clippy exercises, build - // an executable, in addition to running clippy. With a - // compilation failure, this would silently fail. But we expect - // clippy to reflect the same failure while compiling later. - Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to compile!"); - // Due to an issue with Clippy, a cargo clean is required to catch all lints. - // See https://github.com/rust-lang/rust-clippy/issues/2604 - // This is already fixed on Clippy's master branch. See this issue to track merging into Cargo: - // https://github.com/rust-lang/rust-clippy/issues/3837 - Command::new("cargo") - .args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to run 'cargo clean'"); - Command::new("cargo") - .args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .args(["--", "-D", "warnings", "-D", "clippy::float_cmp"]) - .output() - } - } - .expect("Failed to run 'compile' command."); - - if cmd.status.success() { - Ok(CompiledExercise { - exercise: self, - _handle: FileHandle, - }) - } else { - clean(); - Err(ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }) - } + fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { + Command::new("cargo") + .arg(command) + .arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(&self.name) + .args(args) + .output() + .context("Failed to run Cargo") } - fn run(&self) -> Result { - let arg = match self.mode { - Mode::Test => "--show-output", - _ => "", - }; - let cmd = Command::new(temp_file()) - .arg(arg) - .output() - .expect("Failed to run 'run' command"); - - let output = ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }; - - if cmd.status.success() { - Ok(output) - } else { - Err(output) + pub fn run(&self) -> Result { + match self.mode { + Mode::Compile => self.cargo_cmd("run", &[]), + Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]), + Mode::Clippy => self.cargo_cmd( + "clippy", + &["--", "-D", "warnings", "-D", "clippy::float_cmp"], + ), } } @@ -335,51 +225,13 @@ path = "{}.rs""#, impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.path.to_str().unwrap()) + self.path.fmt(f) } } -#[inline] -fn clean() { - let _ignored = remove_file(temp_file()); -} - #[cfg(test)] mod test { use super::*; - use std::path::Path; - - #[test] - fn test_clean() { - File::create(temp_file()).unwrap(); - let exercise = Exercise { - name: String::from("example"), - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::from(""), - }; - let compiled = exercise.compile().unwrap(); - drop(compiled); - assert!(!Path::new(&temp_file()).exists()); - } - - #[test] - #[cfg(target_os = "windows")] - fn test_no_pdb_file() { - [Mode::Compile, Mode::Test] // Clippy doesn't like to test - .iter() - .for_each(|mode| { - let exercise = Exercise { - name: String::from("example"), - // We want a file that does actually compile - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: *mode, - hint: String::from(""), - }; - let _ = exercise.compile().unwrap(); - assert!(!Path::new(&format!("{}.pdb", temp_file())).exists()); - }); - } #[test] fn test_pending_state() { @@ -442,8 +294,8 @@ mod test { mode: Mode::Test, hint: String::new(), }; - let out = exercise.compile().unwrap().run().unwrap(); - assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); + let out = exercise.run().unwrap(); + assert_eq!(out.stdout, b"THIS TEST TOO SHALL PASS"); } #[test] diff --git a/src/main.rs b/src/main.rs index 1926f6ac..1c736f31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,20 +4,18 @@ use crate::verify::verify; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; -use embedded::EMBEDDED_FILES; use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; +use std::io::{BufRead, Write}; use std::path::Path; use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; -use std::thread; use std::time::Duration; +use std::{io, thread}; #[macro_use] mod ui; @@ -94,21 +92,16 @@ fn main() -> Result<()> { println!("\n{WELCOME}\n"); } - if which::which("rustc").is_err() { - println!("We cannot find `rustc`."); - println!("Try running `rustc --version` to diagnose your problem."); - println!("For instructions on how to install Rust, check the README."); + if which::which("cargo").is_err() { + println!( + "Failed to find `cargo`. +Did you already install Rust? +Try running `cargo --version` to diagnose the problem." + ); std::process::exit(1); } - // Read a local `info.toml` if it exists. Mainly to let the tests work for now. - let exercises = if let Ok(file_content) = fs::read_to_string("info.toml") { - toml_edit::de::from_str::(&file_content) - } else { - toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - } - .context("Failed to parse `info.toml`")? - .exercises; + let exercises = ExerciseList::parse()?.exercises; if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; diff --git a/src/run.rs b/src/run.rs index 792bd8fd..2c9f99f6 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,4 +1,5 @@ -use std::io; +use anyhow::{bail, Result}; +use std::io::{self, stdout, Write}; use std::time::Duration; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; @@ -10,13 +11,11 @@ use indicatif::ProgressBar; // 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<(), ()> { +pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> { match exercise.mode { - Mode::Test => test(exercise, verbose)?, - Mode::Compile => compile_and_run(exercise)?, - Mode::Clippy => compile_and_run(exercise)?, + Mode::Test => test(exercise, verbose), + Mode::Compile | Mode::Clippy => compile_and_run(exercise), } - Ok(()) } // Resets the exercise by stashing the changes. @@ -27,41 +26,21 @@ pub fn reset(exercise: &Exercise) -> io::Result<()> { // 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(); - progress_bar.set_message(format!("Compiling {exercise}...")); + progress_bar.set_message(format!("Running {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let compilation_result = exercise.compile(); - let compilation = match compilation_result { - Ok(compilation) => compilation, - Err(output) => { - progress_bar.finish_and_clear(); - warn!( - "Compilation of {} failed!, Compiler error message:\n", - exercise - ); - println!("{}", output.stderr); - return Err(()); - } - }; - - progress_bar.set_message(format!("Running {exercise}...")); - let result = compilation.run(); + let output = exercise.run()?; progress_bar.finish_and_clear(); - match result { - Ok(output) => { - println!("{}", output.stdout); - success!("Successfully ran {}", exercise); - Ok(()) - } - Err(output) => { - println!("{}", output.stdout); - println!("{}", output.stderr); - - warn!("Ran {} with errors", exercise); - Err(()) - } + stdout().write_all(&output.stdout)?; + if !output.status.success() { + stdout().write_all(&output.stderr)?; + warn!("Ran {} with errors", exercise); + bail!("TODO"); } + + success!("Successfully ran {}", exercise); + Ok(()) } diff --git a/src/verify.rs b/src/verify.rs index 5275bf78..56c67796 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,7 +1,14 @@ -use crate::exercise::{CompiledExercise, Exercise, Mode, State}; +use anyhow::{bail, Result}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; -use std::{env, time::Duration}; +use std::{ + env, + io::{stdout, Write}, + process::Output, + time::Duration, +}; + +use crate::exercise::{Exercise, Mode, State}; // Verify that the provided container of Exercise objects // can be compiled and run without any failures. @@ -58,50 +65,44 @@ enum RunMode { } // Compile and run the resulting test harness of the given Exercise -pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> { +pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> { compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?; Ok(()) } // Invoke the rust compiler without running the resulting binary -fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { +fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Compiling {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let _ = compile(exercise, &progress_bar)?; + let _ = exercise.run()?; progress_bar.finish_and_clear(); Ok(prompt_for_completion(exercise, None, success_hints)) } // Compile the given Exercise and run the resulting binary in an interactive mode -fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { +fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); + progress_bar.set_message(format!("Running {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let compilation = compile(exercise, &progress_bar)?; - - progress_bar.set_message(format!("Running {exercise}...")); - let result = compilation.run(); + let output = exercise.run()?; progress_bar.finish_and_clear(); - let output = match result { - Ok(output) => output, - Err(output) => { - warn!("Ran {} with errors", exercise); - println!("{}", output.stdout); - println!("{}", output.stderr); - return Err(()); + if !output.status.success() { + warn!("Ran {} with errors", exercise); + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; } - }; + bail!("TODO"); + } - Ok(prompt_for_completion( - exercise, - Some(output.stdout), - success_hints, - )) + Ok(prompt_for_completion(exercise, Some(output), success_hints)) } // Compile the given Exercise as a test harness and display @@ -111,62 +112,42 @@ fn compile_and_test( run_mode: RunMode, verbose: bool, success_hints: bool, -) -> Result { +) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Testing {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let compilation = compile(exercise, &progress_bar)?; - let result = compilation.run(); + let output = exercise.run()?; progress_bar.finish_and_clear(); - match result { - Ok(output) => { - if verbose { - println!("{}", output.stdout); - } - if run_mode == RunMode::Interactive { - Ok(prompt_for_completion(exercise, None, success_hints)) - } else { - Ok(true) - } - } - Err(output) => { - warn!( - "Testing of {} failed! Please try again. Here's the output:", - exercise - ); - println!("{}", output.stdout); - Err(()) + if !output.status.success() { + warn!( + "Testing of {} failed! Please try again. Here's the output:", + exercise + ); + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; } + bail!("TODO"); } -} -// Compile the given Exercise and return an object with information -// about the state of the compilation -fn compile<'a>( - exercise: &'a Exercise, - progress_bar: &ProgressBar, -) -> Result, ()> { - let compilation_result = exercise.compile(); + if verbose { + stdout().write_all(&output.stdout)?; + } - match compilation_result { - Ok(compilation) => Ok(compilation), - Err(output) => { - progress_bar.finish_and_clear(); - warn!( - "Compiling of {} failed! Please try again. Here's the output:", - exercise - ); - println!("{}", output.stderr); - Err(()) - } + if run_mode == RunMode::Interactive { + Ok(prompt_for_completion(exercise, None, success_hints)) + } else { + Ok(true) } } fn prompt_for_completion( exercise: &Exercise, - prompt_output: Option, + prompt_output: Option, success_hints: bool, ) -> bool { let context = match exercise.state() { @@ -200,10 +181,10 @@ fn prompt_for_completion( } if let Some(output) = prompt_output { - println!( - "Output:\n{separator}\n{output}\n{separator}\n", - separator = separator(), - ); + let separator = separator(); + println!("Output:\n{separator}"); + stdout().write_all(&output.stdout).unwrap(); + println!("\n{separator}\n"); } if success_hints { println!( From c1de4d46aad38d315e061b7262f773f48c6aab63 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 18:25:54 +0200 Subject: [PATCH 20/31] Some improvements to error handling --- src/exercise.rs | 23 ++++++-------- src/main.rs | 84 +++++++++++++++++++++++-------------------------- src/verify.rs | 14 ++++----- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 83d444fc..48aaedd0 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -114,14 +114,9 @@ impl Exercise { } } - pub fn state(&self) -> State { - let source_file = File::open(&self.path).unwrap_or_else(|e| { - println!( - "Failed to open the exercise file {}: {e}", - self.path.display(), - ); - exit(1); - }); + pub fn state(&self) -> Result { + let source_file = File::open(&self.path) + .with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?; let mut source_reader = BufReader::new(source_file); // Read the next line into `buf` without the newline at the end. @@ -152,7 +147,7 @@ impl Exercise { // Reached the end of the file and didn't find the comment. if n == 0 { - return State::Done; + return Ok(State::Done); } if contains_not_done_comment(&line) { @@ -198,7 +193,7 @@ impl Exercise { }); } - return State::Pending(context); + return Ok(State::Pending(context)); } current_line_number += 1; @@ -218,8 +213,8 @@ impl Exercise { // without actually having solved anything. // The only other way to truly check this would to compile and run // the exercise; which would be both costly and counterintuitive - pub fn looks_done(&self) -> bool { - self.state() == State::Done + pub fn looks_done(&self) -> Result { + self.state().map(|state| state == State::Done) } } @@ -271,7 +266,7 @@ mod test { }, ]; - assert_eq!(state, State::Pending(expected)); + assert_eq!(state.unwrap(), State::Pending(expected)); } #[test] @@ -283,7 +278,7 @@ mod test { hint: String::new(), }; - assert_eq!(exercise.state(), State::Done); + assert_eq!(exercise.state().unwrap(), State::Done); } #[test] diff --git a/src/main.rs b/src/main.rs index 1c736f31..72bff4d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,14 +92,11 @@ fn main() -> Result<()> { println!("\n{WELCOME}\n"); } - if which::which("cargo").is_err() { - println!( - "Failed to find `cargo`. + which::which("cargo").context( + "Failed to find `cargo`. Did you already install Rust? -Try running `cargo --version` to diagnose the problem." - ); - std::process::exit(1); - } +Try running `cargo --version` to diagnose the problem.", + )?; let exercises = ExerciseList::parse()?.exercises; @@ -122,7 +119,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); - std::process::exit(0); + exit(0); }); match command { @@ -160,7 +157,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let filter_cond = filters .iter() .any(|f| exercise.name.contains(f) || fname.contains(f)); - let looks_done = exercise.looks_done(); + let looks_done = exercise.looks_done()?; let status = if looks_done { exercises_done += 1; "Done" @@ -185,8 +182,8 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let mut handle = stdout.lock(); handle.write_all(line.as_bytes()).unwrap_or_else(|e| { match e.kind() { - std::io::ErrorKind::BrokenPipe => std::process::exit(0), - _ => std::process::exit(1), + std::io::ErrorKind::BrokenPipe => exit(0), + _ => exit(1), }; }); } @@ -200,35 +197,34 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exercises.len(), percentage_progress ); - std::process::exit(0); + exit(0); } Subcommands::Run { name } => { - let exercise = find_exercise(&name, &exercises); - run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); + let exercise = find_exercise(&name, &exercises)?; + run(exercise, verbose).unwrap_or_else(|_| exit(1)); } Subcommands::Reset { name } => { - let exercise = find_exercise(&name, &exercises); + let exercise = find_exercise(&name, &exercises)?; reset(exercise)?; println!("The file {} has been reset!", exercise.path.display()); } Subcommands::Hint { name } => { - let exercise = find_exercise(&name, &exercises); + let exercise = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } Subcommands::Verify => { - verify(&exercises, (0, exercises.len()), verbose, false) - .unwrap_or_else(|_| std::process::exit(1)); + verify(&exercises, (0, exercises.len()), verbose, false).unwrap_or_else(|_| exit(1)); } Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Err(e) => { println!("Error: Could not watch your progress. Error message was {e:?}."); println!("Most likely you've run out of disk space or your 'inotify limit' has been reached."); - std::process::exit(1); + exit(1); } Ok(WatchStatus::Finished) => { println!( @@ -295,25 +291,23 @@ fn spawn_watch_shell( }); } -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise { +fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> { if name == "next" { - exercises - .iter() - .find(|e| !e.looks_done()) - .unwrap_or_else(|| { - println!("🎉 Congratulations! You have done all the exercises!"); - println!("🔚 There are no more exercises to do next!"); - std::process::exit(1) - }) - } else { - exercises - .iter() - .find(|e| e.name == name) - .unwrap_or_else(|| { - println!("No exercise found for '{name}'!"); - std::process::exit(1) - }) + for exercise in exercises { + if !exercise.looks_done()? { + return Ok(exercise); + } + } + + println!("🎉 Congratulations! You have done all the exercises!"); + println!("🔚 There are no more exercises to do next!"); + exit(0); } + + exercises + .iter() + .find(|e| e.name == name) + .with_context(|| format!("No exercise found for '{name}'!")) } enum WatchStatus { @@ -363,17 +357,17 @@ fn watch( && event_path.exists() { let filepath = event_path.as_path().canonicalize().unwrap(); - let pending_exercises = - exercises - .iter() - .find(|e| filepath.ends_with(&e.path)) - .into_iter() - .chain(exercises.iter().filter(|e| { - !e.looks_done() && !filepath.ends_with(&e.path) - })); + // TODO: Remove unwrap + let pending_exercises = exercises + .iter() + .find(|e| filepath.ends_with(&e.path)) + .into_iter() + .chain(exercises.iter().filter(|e| { + !e.looks_done().unwrap() && !filepath.ends_with(&e.path) + })); let num_done = exercises .iter() - .filter(|e| e.looks_done() && !filepath.ends_with(&e.path)) + .filter(|e| e.looks_done().unwrap() && !filepath.ends_with(&e.path)) .count(); clear_screen(); match verify( diff --git a/src/verify.rs b/src/verify.rs index 56c67796..adfd3b26 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -79,7 +79,7 @@ fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { let _ = exercise.run()?; progress_bar.finish_and_clear(); - Ok(prompt_for_completion(exercise, None, success_hints)) + prompt_for_completion(exercise, None, success_hints) } // Compile the given Exercise and run the resulting binary in an interactive mode @@ -102,7 +102,7 @@ fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Re bail!("TODO"); } - Ok(prompt_for_completion(exercise, Some(output), success_hints)) + prompt_for_completion(exercise, Some(output), success_hints) } // Compile the given Exercise as a test harness and display @@ -139,7 +139,7 @@ fn compile_and_test( } if run_mode == RunMode::Interactive { - Ok(prompt_for_completion(exercise, None, success_hints)) + prompt_for_completion(exercise, None, success_hints) } else { Ok(true) } @@ -149,9 +149,9 @@ fn prompt_for_completion( exercise: &Exercise, prompt_output: Option, success_hints: bool, -) -> bool { - let context = match exercise.state() { - State::Done => return true, +) -> Result { + let context = match exercise.state()? { + State::Done => return Ok(true), State::Pending(context) => context, }; match exercise.mode { @@ -215,7 +215,7 @@ fn prompt_for_completion( ); } - false + Ok(false) } fn separator() -> console::StyledObject<&'static str> { From 7090fffeae88a2afdeb42ae3301c4842416ab729 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 18:59:01 +0200 Subject: [PATCH 21/31] Fix tests --- .gitignore | 7 +++---- Cargo.toml | 5 +++++ tests/fixture/failure/Cargo.toml | 21 +++++++++++++++++++++ tests/fixture/state/Cargo.toml | 17 +++++++++++++++++ tests/fixture/success/Cargo.toml | 13 +++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/fixture/failure/Cargo.toml create mode 100644 tests/fixture/state/Cargo.toml create mode 100644 tests/fixture/success/Cargo.toml diff --git a/.gitignore b/.gitignore index f319d39d..d6c77083 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ -*.swp target/ +tests/fixture/*/Cargo.lock + +*.swp **/*.rs.bk .DS_Store *.pdb -exercises/clippy/Cargo.toml -exercises/clippy/Cargo.lock -rust-project.json .idea .vscode/* !.vscode/extensions.json diff --git a/Cargo.toml b/Cargo.toml index 9224364b..5fc75f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] resolver = "2" +exclude = [ + "tests/fixture/failure", + "tests/fixture/state", + "tests/fixture/success", +] [workspace.package] version = "6.0.0" diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml new file mode 100644 index 00000000..dd728c34 --- /dev/null +++ b/tests/fixture/failure/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "compFailure" +path = "exercises/compFailure.rs" + +[[bin]] +name = "compNoExercise" +path = "exercises/compNoExercise.rs" + +[[bin]] +name = "testFailure" +path = "exercises/testFailure.rs" + +[[bin]] +name = "testNotPassed" +path = "exercises/testNotPassed.rs" diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml new file mode 100644 index 00000000..5cfa42ba --- /dev/null +++ b/tests/fixture/state/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "pending_exercise" +path = "exercises/pending_exercise.rs" + +[[bin]] +name = "pending_test_exercise" +path = "exercises/pending_test_exercise.rs" + +[[bin]] +name = "finished_exercise" +path = "exercises/finished_exercise.rs" diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml new file mode 100644 index 00000000..c0059284 --- /dev/null +++ b/tests/fixture/success/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "compSuccess" +path = "exercises/compSuccess.rs" + +[[bin]] +name = "testSuccess" +path = "exercises/testSuccess.rs" From fb32d0b86fd2f3f0c1e82fecbf2cf4931a7b1ff5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 18:59:07 +0200 Subject: [PATCH 22/31] Remove redundant test --- src/exercise.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 48aaedd0..e7045d60 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -281,18 +281,6 @@ mod test { assert_eq!(exercise.state().unwrap(), State::Done); } - #[test] - fn test_exercise_with_output() { - let exercise = Exercise { - name: "exercise_with_output".into(), - path: PathBuf::from("tests/fixture/success/exercises/testSuccess.rs"), - mode: Mode::Test, - hint: String::new(), - }; - let out = exercise.run().unwrap(); - assert_eq!(out.stdout, b"THIS TEST TOO SHALL PASS"); - } - #[test] fn test_not_done() { assert!(contains_not_done_comment("// I AM NOT DONE")); From 7560aec66b4a109c32ea59daa65580ab2ac26333 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 20:08:23 +0200 Subject: [PATCH 23/31] Inline reset --- src/main.rs | 7 +++++-- src/run.rs | 11 ++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 72bff4d9..0f298dde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; -use crate::run::{reset, run}; +use crate::run::run; use crate::verify::verify; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; @@ -207,7 +208,9 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Subcommands::Reset { name } => { let exercise = find_exercise(&name, &exercises)?; - reset(exercise)?; + EMBEDDED_FILES + .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) + .with_context(|| format!("Failed to reset the exercise {exercise}"))?; println!("The file {} has been reset!", exercise.path.display()); } diff --git a/src/run.rs b/src/run.rs index 2c9f99f6..3f93f146 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,8 +1,7 @@ use anyhow::{bail, Result}; -use std::io::{self, stdout, Write}; +use std::io::{stdout, Write}; use std::time::Duration; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, Mode}; use crate::verify::test; use indicatif::ProgressBar; @@ -18,13 +17,7 @@ pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> { } } -// Resets the exercise by stashing the changes. -pub fn reset(exercise: &Exercise) -> io::Result<()> { - EMBEDDED_FILES.write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) -} - -// Invoke the rust compiler on the path of the given exercise -// and run the ensuing binary. +// Compile and run an exercise. // This is strictly for non-test binaries, so output is displayed fn compile_and_run(exercise: &Exercise) -> Result<()> { let progress_bar = ProgressBar::new_spinner(); From 8ad18de54cdad2e94d40d7d4cb67e4a6a274c293 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 20:11:08 +0200 Subject: [PATCH 24/31] Use var_os to avoid conversion to String --- src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index d8177b9f..22d60d96 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,7 +3,7 @@ macro_rules! print_emoji { use console::{style, Emoji}; use std::env; let formatstr = format!($fmt, $ex); - if env::var("NO_EMOJI").is_ok() { + if env::var_os("NO_EMOJI").is_some() { println!("{} {}", style($sign).$color(), style(formatstr).$color()); } else { println!( From 14f3585816ae12091956efcc45c1e4aefc2f91ce Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 02:11:52 +0200 Subject: [PATCH 25/31] Make `cargo run` work --- .gitignore | 3 +- Cargo.toml | 2 + dev/Cargo.toml | 104 ++++++++++++++++++++++++++++++++++ src/bin/gen-dev-cargo-toml.rs | 56 ++++++++++++++++++ src/exercise.rs | 14 ++++- tests/dev_cargo_bins.rs | 39 +++++++++++++ 6 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 dev/Cargo.toml create mode 100644 src/bin/gen-dev-cargo-toml.rs create mode 100644 tests/dev_cargo_bins.rs diff --git a/.gitignore b/.gitignore index d6c77083..0bbbc542 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ -tests/fixture/*/Cargo.lock +/tests/fixture/*/Cargo.lock +/dev/Cargo.lock *.swp **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index 5fc75f98..86187b4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ exclude = [ "tests/fixture/failure", "tests/fixture/state", "tests/fixture/success", + "dev", ] [workspace.package] @@ -18,6 +19,7 @@ edition = "2021" [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" +default-run = "rustlings" version.workspace = true authors.workspace = true license.workspace = true diff --git a/dev/Cargo.toml b/dev/Cargo.toml new file mode 100644 index 00000000..4ad48866 --- /dev/null +++ b/dev/Cargo.toml @@ -0,0 +1,104 @@ +bin = [ + { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, + { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, + { name = "variables1", path = "../exercises/01_variables/variables1.rs" }, + { name = "variables2", path = "../exercises/01_variables/variables2.rs" }, + { name = "variables3", path = "../exercises/01_variables/variables3.rs" }, + { name = "variables4", path = "../exercises/01_variables/variables4.rs" }, + { name = "variables5", path = "../exercises/01_variables/variables5.rs" }, + { name = "variables6", path = "../exercises/01_variables/variables6.rs" }, + { name = "functions1", path = "../exercises/02_functions/functions1.rs" }, + { name = "functions2", path = "../exercises/02_functions/functions2.rs" }, + { name = "functions3", path = "../exercises/02_functions/functions3.rs" }, + { name = "functions4", path = "../exercises/02_functions/functions4.rs" }, + { name = "functions5", path = "../exercises/02_functions/functions5.rs" }, + { name = "if1", path = "../exercises/03_if/if1.rs" }, + { name = "if2", path = "../exercises/03_if/if2.rs" }, + { name = "if3", path = "../exercises/03_if/if3.rs" }, + { name = "quiz1", path = "../exercises/quiz1.rs" }, + { name = "primitive_types1", path = "../exercises/04_primitive_types/primitive_types1.rs" }, + { name = "primitive_types2", path = "../exercises/04_primitive_types/primitive_types2.rs" }, + { name = "primitive_types3", path = "../exercises/04_primitive_types/primitive_types3.rs" }, + { name = "primitive_types4", path = "../exercises/04_primitive_types/primitive_types4.rs" }, + { name = "primitive_types5", path = "../exercises/04_primitive_types/primitive_types5.rs" }, + { name = "primitive_types6", path = "../exercises/04_primitive_types/primitive_types6.rs" }, + { name = "vecs1", path = "../exercises/05_vecs/vecs1.rs" }, + { name = "vecs2", path = "../exercises/05_vecs/vecs2.rs" }, + { name = "move_semantics1", path = "../exercises/06_move_semantics/move_semantics1.rs" }, + { name = "move_semantics2", path = "../exercises/06_move_semantics/move_semantics2.rs" }, + { name = "move_semantics3", path = "../exercises/06_move_semantics/move_semantics3.rs" }, + { name = "move_semantics4", path = "../exercises/06_move_semantics/move_semantics4.rs" }, + { name = "move_semantics5", path = "../exercises/06_move_semantics/move_semantics5.rs" }, + { name = "move_semantics6", path = "../exercises/06_move_semantics/move_semantics6.rs" }, + { name = "structs1", path = "../exercises/07_structs/structs1.rs" }, + { name = "structs2", path = "../exercises/07_structs/structs2.rs" }, + { name = "structs3", path = "../exercises/07_structs/structs3.rs" }, + { name = "enums1", path = "../exercises/08_enums/enums1.rs" }, + { name = "enums2", path = "../exercises/08_enums/enums2.rs" }, + { name = "enums3", path = "../exercises/08_enums/enums3.rs" }, + { name = "strings1", path = "../exercises/09_strings/strings1.rs" }, + { name = "strings2", path = "../exercises/09_strings/strings2.rs" }, + { name = "strings3", path = "../exercises/09_strings/strings3.rs" }, + { name = "strings4", path = "../exercises/09_strings/strings4.rs" }, + { name = "modules1", path = "../exercises/10_modules/modules1.rs" }, + { name = "modules2", path = "../exercises/10_modules/modules2.rs" }, + { name = "modules3", path = "../exercises/10_modules/modules3.rs" }, + { name = "hashmaps1", path = "../exercises/11_hashmaps/hashmaps1.rs" }, + { name = "hashmaps2", path = "../exercises/11_hashmaps/hashmaps2.rs" }, + { name = "hashmaps3", path = "../exercises/11_hashmaps/hashmaps3.rs" }, + { name = "quiz2", path = "../exercises/quiz2.rs" }, + { name = "options1", path = "../exercises/12_options/options1.rs" }, + { name = "options2", path = "../exercises/12_options/options2.rs" }, + { name = "options3", path = "../exercises/12_options/options3.rs" }, + { name = "errors1", path = "../exercises/13_error_handling/errors1.rs" }, + { name = "errors2", path = "../exercises/13_error_handling/errors2.rs" }, + { name = "errors3", path = "../exercises/13_error_handling/errors3.rs" }, + { name = "errors4", path = "../exercises/13_error_handling/errors4.rs" }, + { name = "errors5", path = "../exercises/13_error_handling/errors5.rs" }, + { name = "errors6", path = "../exercises/13_error_handling/errors6.rs" }, + { name = "generics1", path = "../exercises/14_generics/generics1.rs" }, + { name = "generics2", path = "../exercises/14_generics/generics2.rs" }, + { name = "traits1", path = "../exercises/15_traits/traits1.rs" }, + { name = "traits2", path = "../exercises/15_traits/traits2.rs" }, + { name = "traits3", path = "../exercises/15_traits/traits3.rs" }, + { name = "traits4", path = "../exercises/15_traits/traits4.rs" }, + { name = "traits5", path = "../exercises/15_traits/traits5.rs" }, + { name = "quiz3", path = "../exercises/quiz3.rs" }, + { name = "lifetimes1", path = "../exercises/16_lifetimes/lifetimes1.rs" }, + { name = "lifetimes2", path = "../exercises/16_lifetimes/lifetimes2.rs" }, + { name = "lifetimes3", path = "../exercises/16_lifetimes/lifetimes3.rs" }, + { name = "tests1", path = "../exercises/17_tests/tests1.rs" }, + { name = "tests2", path = "../exercises/17_tests/tests2.rs" }, + { name = "tests3", path = "../exercises/17_tests/tests3.rs" }, + { name = "tests4", path = "../exercises/17_tests/tests4.rs" }, + { name = "iterators1", path = "../exercises/18_iterators/iterators1.rs" }, + { name = "iterators2", path = "../exercises/18_iterators/iterators2.rs" }, + { name = "iterators3", path = "../exercises/18_iterators/iterators3.rs" }, + { name = "iterators4", path = "../exercises/18_iterators/iterators4.rs" }, + { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, + { name = "box1", path = "../exercises/19_smart_pointers/box1.rs" }, + { name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" }, + { name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" }, + { name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" }, + { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, + { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, + { name = "threads3", path = "../exercises/20_threads/threads3.rs" }, + { name = "macros1", path = "../exercises/21_macros/macros1.rs" }, + { name = "macros2", path = "../exercises/21_macros/macros2.rs" }, + { name = "macros3", path = "../exercises/21_macros/macros3.rs" }, + { name = "macros4", path = "../exercises/21_macros/macros4.rs" }, + { name = "clippy1", path = "../exercises/22_clippy/clippy1.rs" }, + { name = "clippy2", path = "../exercises/22_clippy/clippy2.rs" }, + { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, + { name = "using_as", path = "../exercises/23_conversions/using_as.rs" }, + { name = "from_into", path = "../exercises/23_conversions/from_into.rs" }, + { name = "from_str", path = "../exercises/23_conversions/from_str.rs" }, + { name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" }, + { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, +] + +[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs new file mode 100644 index 00000000..20167a1e --- /dev/null +++ b/src/bin/gen-dev-cargo-toml.rs @@ -0,0 +1,56 @@ +use anyhow::{bail, Context, Result}; +use serde::Deserialize; +use std::{ + fs::{self, create_dir}, + io::ErrorKind, +}; + +#[derive(Deserialize)] +struct Exercise { + name: String, + path: String, +} + +#[derive(Deserialize)] +struct InfoToml { + exercises: Vec, +} + +fn main() -> Result<()> { + let exercises = toml_edit::de::from_str::( + &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?, + ) + .context("Failed to deserialize `info.toml`")? + .exercises; + + let mut buf = Vec::with_capacity(1 << 14); + + buf.extend_from_slice(b"bin = [\n"); + + for exercise in exercises { + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise.name.as_bytes()); + buf.extend_from_slice(b"\", path = \"../"); + buf.extend_from_slice(exercise.path.as_bytes()); + buf.extend_from_slice(b"\" },\n"); + } + + buf.extend_from_slice( + br#"] + +[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false +"#, + ); + + if let Err(e) = create_dir("dev") { + if e.kind() != ErrorKind::AlreadyExists { + bail!("Failed to create the `dev` directory: {e}"); + } + } + + fs::write("dev/Cargo.toml", buf).context("Failed to write `dev/Cargo.toml`") +} diff --git a/src/exercise.rs b/src/exercise.rs index e7045d60..450acf45 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -91,9 +91,17 @@ pub struct ContextLine { impl Exercise { fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { - Command::new("cargo") - .arg(command) - .arg("--color") + let mut cmd = Command::new("cargo"); + cmd.arg(command); + + // A hack to make `cargo run` work when developing Rustlings. + // Use `dev/Cargo.toml` when in the directory of the repository. + #[cfg(debug_assertions)] + if std::path::Path::new("tests").exists() { + cmd.arg("--manifest-path").arg("dev/Cargo.toml"); + } + + cmd.arg("--color") .arg("always") .arg("-q") .arg("--bin") diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs new file mode 100644 index 00000000..7f1771b6 --- /dev/null +++ b/tests/dev_cargo_bins.rs @@ -0,0 +1,39 @@ +// Makes sure that `dev/Cargo.toml` is synced with `info.toml`. +// When this test fails, you just need to run `cargo run --bin gen-dev-cargo-toml`. + +use serde::Deserialize; +use std::fs; + +#[derive(Deserialize)] +struct Exercise { + name: String, + path: String, +} + +#[derive(Deserialize)] +struct InfoToml { + exercises: Vec, +} + +#[test] +fn dev_cargo_bins() { + let content = fs::read_to_string("exercises/Cargo.toml").unwrap(); + + let exercises = toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) + .unwrap() + .exercises; + + let mut start_ind = 0; + for exercise in exercises { + let name_start = start_ind + content[start_ind..].find('"').unwrap() + 1; + let name_end = name_start + content[name_start..].find('"').unwrap(); + assert_eq!(exercise.name, &content[name_start..name_end]); + + // +3 to skip `../` at the begeinning of the path. + let path_start = name_end + content[name_end + 1..].find('"').unwrap() + 5; + let path_end = path_start + content[path_start..].find('"').unwrap(); + assert_eq!(exercise.path, &content[path_start..path_end]); + + start_ind = path_end + 1; + } +} From 2f30eac27f2b57148081dbe1c489e6c47f01d6a9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 17:36:42 +0200 Subject: [PATCH 26/31] Remove unneeded .iter() --- src/main.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0f298dde..f9e0f830 100644 --- a/src/main.rs +++ b/src/main.rs @@ -339,12 +339,8 @@ fn watch( clear_screen(); - let failed_exercise_hint = match verify( - exercises.iter(), - (0, exercises.len()), - verbose, - success_hints, - ) { + let failed_exercise_hint = match verify(exercises, (0, exercises.len()), verbose, success_hints) + { Ok(_) => return Ok(WatchStatus::Finished), Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), }; From fdd7de00bd37e43a4e464d1cb5cc10c3753b3688 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 18:21:56 +0200 Subject: [PATCH 27/31] Improvements to `verify` --- src/main.rs | 37 ++++++++++++++++--------------------- src/verify.rs | 17 ++++++----------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index f9e0f830..7b7b1655 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use console::Emoji; use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; -use std::ffi::OsStr; use std::io::{BufRead, Write}; use std::path::Path; use std::process::{exit, Command}; @@ -344,44 +343,40 @@ fn watch( Ok(_) => return Ok(WatchStatus::Finished), Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), }; + spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); + + let mut pending_exercises = Vec::with_capacity(exercises.len()); loop { match rx.recv_timeout(Duration::from_secs(1)) { Ok(event) => match event { Ok(events) => { for event in events { - let event_path = event.path; if event.kind == DebouncedEventKind::Any - && event_path.extension() == Some(OsStr::new("rs")) - && event_path.exists() + && event.path.extension().is_some_and(|ext| ext == "rs") { - let filepath = event_path.as_path().canonicalize().unwrap(); - // TODO: Remove unwrap - let pending_exercises = exercises - .iter() - .find(|e| filepath.ends_with(&e.path)) - .into_iter() - .chain(exercises.iter().filter(|e| { - !e.looks_done().unwrap() && !filepath.ends_with(&e.path) - })); - let num_done = exercises - .iter() - .filter(|e| e.looks_done().unwrap() && !filepath.ends_with(&e.path)) - .count(); + pending_exercises.extend(exercises.iter().filter(|exercise| { + !exercise.looks_done().unwrap_or(false) + || event.path.ends_with(&exercise.path) + })); + let num_done = exercises.len() - pending_exercises.len(); + clear_screen(); + match verify( - pending_exercises, + pending_exercises.iter().copied(), (num_done, exercises.len()), verbose, success_hints, ) { Ok(_) => return Ok(WatchStatus::Finished), Err(exercise) => { - let mut failed_exercise_hint = - failed_exercise_hint.lock().unwrap(); - *failed_exercise_hint = Some(exercise.hint.clone()); + let hint = exercise.hint.clone(); + *failed_exercise_hint.lock().unwrap() = Some(hint); } } + + pending_exercises.clear(); } } } diff --git a/src/verify.rs b/src/verify.rs index adfd3b26..6e048a19 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -16,7 +16,7 @@ use crate::exercise::{Exercise, Mode, State}; // 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>( - exercises: impl IntoIterator, + pending_exercises: impl IntoIterator, progress: (usize, usize), verbose: bool, success_hints: bool, @@ -33,7 +33,7 @@ pub fn verify<'a>( bar.set_position(num_done as u64); bar.set_message(format!("({percentage:.1} %)")); - for exercise in exercises { + for exercise in pending_exercises { let compile_result = match exercise.mode { Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), Mode::Compile => compile_and_run_interactively(exercise, success_hints), @@ -45,16 +45,11 @@ pub fn verify<'a>( percentage += 100.0 / total as f32; bar.inc(1); bar.set_message(format!("({percentage:.1} %)")); - if bar.position() == total as u64 { - println!( - "Progress: You completed {} / {} exercises ({:.1} %).", - bar.position(), - total, - percentage - ); - bar.finish(); - } } + + bar.finish(); + println!("You completed all exercises!"); + Ok(()) } From def8d2c569a8a637396960c8513a0b1bdf88ef0c Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 18:38:01 +0200 Subject: [PATCH 28/31] Add VerifyState --- src/main.rs | 34 ++++++++++++++++------------------ src/verify.rs | 19 ++++++++++++------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7b7b1655..c8c65848 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; use crate::run::run; use crate::verify::verify; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; -use notify_debouncer_mini::notify::{self, RecursiveMode}; +use notify_debouncer_mini::notify::RecursiveMode; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::io::{BufRead, Write}; @@ -16,6 +16,7 @@ use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{io, thread}; +use verify::VerifyState; #[macro_use] mod ui; @@ -218,9 +219,10 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini println!("{}", exercise.hint); } - Subcommands::Verify => { - verify(&exercises, (0, exercises.len()), verbose, false).unwrap_or_else(|_| exit(1)); - } + Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? { + VerifyState::AllExercisesDone => println!("All exercises done!"), + VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), + }, Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Err(e) => { @@ -317,11 +319,7 @@ enum WatchStatus { Unfinished, } -fn watch( - exercises: &[Exercise], - verbose: bool, - success_hints: bool, -) -> notify::Result { +fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result { /* Clears the terminal with an ANSI escape code. Works in UNIX and newer Windows terminals. */ fn clear_screen() { @@ -338,11 +336,11 @@ fn watch( clear_screen(); - let failed_exercise_hint = match verify(exercises, (0, exercises.len()), verbose, success_hints) - { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), - }; + let failed_exercise_hint = + match verify(exercises, (0, exercises.len()), verbose, success_hints)? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), + }; spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); @@ -368,9 +366,9 @@ fn watch( (num_done, exercises.len()), verbose, success_hints, - ) { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => { + )? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => { let hint = exercise.hint.clone(); *failed_exercise_hint.lock().unwrap() = Some(hint); } diff --git a/src/verify.rs b/src/verify.rs index 6e048a19..02bff995 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -10,6 +10,11 @@ use std::{ use crate::exercise::{Exercise, Mode, State}; +pub enum VerifyState<'a> { + AllExercisesDone, + Failed(&'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. @@ -20,7 +25,7 @@ pub fn verify<'a>( progress: (usize, usize), verbose: bool, success_hints: bool, -) -> Result<(), &'a Exercise> { +) -> Result> { let (num_done, total) = progress; let bar = ProgressBar::new(total as u64); let mut percentage = num_done as f32 / total as f32 * 100.0; @@ -35,12 +40,12 @@ pub fn verify<'a>( for exercise in pending_exercises { let compile_result = match exercise.mode { - Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), - Mode::Compile => compile_and_run_interactively(exercise, success_hints), - Mode::Clippy => compile_only(exercise, success_hints), + Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?, + Mode::Compile => compile_and_run_interactively(exercise, success_hints)?, + Mode::Clippy => compile_only(exercise, success_hints)?, }; - if !compile_result.unwrap_or(false) { - return Err(exercise); + if !compile_result { + return Ok(VerifyState::Failed(exercise)); } percentage += 100.0 / total as f32; bar.inc(1); @@ -50,7 +55,7 @@ pub fn verify<'a>( bar.finish(); println!("You completed all exercises!"); - Ok(()) + Ok(VerifyState::AllExercisesDone) } #[derive(PartialEq, Eq)] From 190945352a2316154d9856a5d882893326e0136a Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 18:52:43 +0200 Subject: [PATCH 29/31] Add comments about dev/Cargo.toml --- dev/Cargo.toml | 3 +++ src/bin/gen-dev-cargo-toml.rs | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 4ad48866..e4e7be78 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,3 +1,6 @@ +# This file is a hack to allow using `cargo r` to test `rustlings` during development. +# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`. + bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs index 20167a1e..65cc2447 100644 --- a/src/bin/gen-dev-cargo-toml.rs +++ b/src/bin/gen-dev-cargo-toml.rs @@ -1,3 +1,7 @@ +// Generates `dev/Cargo.toml` such that it is synced with `info.toml`. +// `dev/Cargo.toml` is a hack to allow using `cargo r` to test `rustlings` +// during development. + use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{ @@ -25,7 +29,12 @@ fn main() -> Result<()> { let mut buf = Vec::with_capacity(1 << 14); - buf.extend_from_slice(b"bin = [\n"); + buf.extend_from_slice( + b"# This file is a hack to allow using `cargo r` to test `rustlings` during development. +# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`. + +bin = [\n", + ); for exercise in exercises { buf.extend_from_slice(b" { name = \""); From 569a68eb73b82040588138b0ba1daabca1a7d415 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 15:44:48 +0200 Subject: [PATCH 30/31] Minify generated Cargo.toml --- src/init.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/init.rs b/src/init.rs index d958c96e..b52b6139 100644 --- a/src/init.rs +++ b/src/init.rs @@ -10,21 +10,25 @@ use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); + cargo_toml.extend_from_slice(b"bin = [\n"); + for exercise in exercises { + cargo_toml.extend_from_slice(b" { name = \""); + cargo_toml.extend_from_slice(exercise.name.as_bytes()); + cargo_toml.extend_from_slice(b"\", path = \""); + cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); + cargo_toml.extend_from_slice(b"\" },\n"); + } + cargo_toml.extend_from_slice( - br#"[package] + br#"] + +[package] name = "rustlings" version = "0.0.0" edition = "2021" publish = false "#, ); - for exercise in exercises { - cargo_toml.extend_from_slice(b"\n[[bin]]\nname = \""); - cargo_toml.extend_from_slice(exercise.name.as_bytes()); - cargo_toml.extend_from_slice(b"\"\npath = \""); - cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); - cargo_toml.extend_from_slice(b"\"\n"); - } OpenOptions::new() .create_new(true) .write(true) From b6c434c445d91a9e886e5639b078635e5eca4eb3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 15:45:53 +0200 Subject: [PATCH 31/31] Remove optional version field --- dev/Cargo.toml | 1 - src/bin/gen-dev-cargo-toml.rs | 1 - src/init.rs | 1 - tests/fixture/failure/Cargo.toml | 1 - tests/fixture/state/Cargo.toml | 1 - tests/fixture/success/Cargo.toml | 1 - 6 files changed, 6 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index e4e7be78..7868b97c 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -102,6 +102,5 @@ bin = [ [package] name = "rustlings" -version = "0.0.0" edition = "2021" publish = false diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs index 65cc2447..ff8f31db 100644 --- a/src/bin/gen-dev-cargo-toml.rs +++ b/src/bin/gen-dev-cargo-toml.rs @@ -49,7 +49,6 @@ bin = [\n", [package] name = "rustlings" -version = "0.0.0" edition = "2021" publish = false "#, diff --git a/src/init.rs b/src/init.rs index b52b6139..6af32351 100644 --- a/src/init.rs +++ b/src/init.rs @@ -24,7 +24,6 @@ fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { [package] name = "rustlings" -version = "0.0.0" edition = "2021" publish = false "#, diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml index dd728c34..e111cf2b 100644 --- a/tests/fixture/failure/Cargo.toml +++ b/tests/fixture/failure/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "tests" -version = "0.0.0" edition = "2021" publish = false diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml index 5cfa42ba..c8d74e47 100644 --- a/tests/fixture/state/Cargo.toml +++ b/tests/fixture/state/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "tests" -version = "0.0.0" edition = "2021" publish = false diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml index c0059284..f26a44f1 100644 --- a/tests/fixture/success/Cargo.toml +++ b/tests/fixture/success/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "tests" -version = "0.0.0" edition = "2021" publish = false