diff --git a/.gitignore b/.gitignore index 32f3c77..0bbbc54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -*.swp target/ +/tests/fixture/*/Cargo.lock +/dev/Cargo.lock + +*.swp **/*.rs.bk .DS_Store *.pdb -exercises/22_clippy/Cargo.toml -exercises/22_clippy/Cargo.lock -rust-project.json .idea .vscode/* !.vscode/extensions.json diff --git a/Cargo.lock b/Cargo.lock index f4853d0..d8e5b72 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", @@ -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 = "6.0.0" +dependencies = [ + "quote", +] + [[package]] name = "ryu" version = "1.0.17" diff --git a/Cargo.toml b/Cargo.toml index 2d152cf..86187b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,37 @@ -[package] -name = "rustlings" -description = "Small exercises to get you used to reading and writing Rust code!" -version = "5.6.1" +[workspace] +resolver = "2" +exclude = [ + "tests/fixture/failure", + "tests/fixture/state", + "tests/fixture/success", + "dev", +] + +[workspace.package] +version = "6.0.0" 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!" +default-run = "rustlings" +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,11 +39,13 @@ 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" predicates = "3.1.0" + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" diff --git a/dev/Cargo.toml b/dev/Cargo.toml new file mode 100644 index 0000000..7868b97 --- /dev/null +++ b/dev/Cargo.toml @@ -0,0 +1,106 @@ +# 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" }, + { 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" +edition = "2021" +publish = false diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 844b013..0000000 --- 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 fdbe8d4..0000000 --- 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." diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml new file mode 100644 index 0000000..0114c8f --- /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 0000000..598b5c3 --- /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: ::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!(::std::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: &[#( + EmbeddedFile { + path: #files, + content: ::std::include_bytes!(::std::concat!("../", #files)), + } + ),*], + dirs: &[#(#dirs),*], + }, + } + } + .into() +} diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs new file mode 100644 index 0000000..ff8f31d --- /dev/null +++ b/src/bin/gen-dev-cargo-toml.rs @@ -0,0 +1,64 @@ +// 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::{ + 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"# 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 = \""); + 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" +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/embedded.rs b/src/embedded.rs new file mode 100644 index 0000000..56b4b61 --- /dev/null +++ b/src/embedded.rs @@ -0,0 +1,117 @@ +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)?; + + Ok(()) + } +} + +struct ExercisesDir { + readme: EmbeddedFile, + files: &'static [EmbeddedFile], + dirs: &'static [EmbeddedFlatDir], +} + +pub struct EmbeddedFiles { + 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::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<()> { + if let Some(file) = self + .exercises_dir + .files + .iter() + .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| Path::new(file.path) == path) { + dir.init_on_disk()?; + return file.write_to_disk(strategy); + } + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("{} not found in the embedded files", path.display()), + )) + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 19f528a..450acf4 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,182 +45,86 @@ 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."); + fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { + let mut cmd = Command::new("cargo"); + cmd.arg(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(), - }) + // 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"); } - } - fn run(&self) -> Result { - let arg = match self.mode { - Mode::Test => "--show-output", - _ => "", - }; - let cmd = Command::new(temp_file()) - .arg(arg) + cmd.arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(&self.name) + .args(args) .output() - .expect("Failed to run 'run' command"); + .context("Failed to run Cargo") + } - 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"], + ), } } - 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. @@ -262,7 +155,7 @@ path = "{}.rs""#, // 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) { @@ -308,7 +201,7 @@ path = "{}.rs""#, }); } - return State::Pending(context); + return Ok(State::Pending(context)); } current_line_number += 1; @@ -328,64 +221,26 @@ path = "{}.rs""#, // 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) } } 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/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/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() { 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(), }; @@ -419,31 +274,19 @@ mod test { }, ]; - assert_eq!(state, State::Pending(expected)); + assert_eq!(state.unwrap(), State::Pending(expected)); } #[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(), }; - assert_eq!(exercise.state(), State::Done); - } - - #[test] - fn test_exercise_with_output() { - let exercise = Exercise { - name: "exercise_with_output".into(), - path: PathBuf::from("tests/fixture/success/testSuccess.rs"), - mode: Mode::Test, - hint: String::new(), - }; - let out = exercise.compile().unwrap().run().unwrap(); - assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); + assert_eq!(exercise.state().unwrap(), State::Done); } #[test] diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 0000000..6af3235 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,97 @@ +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(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] +name = "rustlings" +edition = "2021" +publish = false +"#, + ); + OpenOptions::new() + .create_new(true) + .write(true) + .open("Cargo.toml")? + .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"]}"#; + 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<()> { + 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 { + bail!( + "A directory with the name `rustlings` 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_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; + + create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 8f73dbb..c8c6584 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,29 @@ +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; -use crate::project::write_project_json; -use crate::run::{reset, run}; +use crate::run::run; use crate::verify::verify; -use anyhow::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::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; +use std::io::{BufRead, Write}; 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}; -use std::thread; use std::time::Duration; +use std::{io, thread}; +use verify::VerifyState; #[macro_use] mod ui; +mod embedded; mod exercise; -mod project; +mod init; mod run; mod verify; @@ -40,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 @@ -53,7 +55,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, @@ -82,8 +84,6 @@ enum Subcommands { #[arg(short, long)] solved: bool, }, - /// Enable rust-analyzer for exercises - Lsp, } fn main() -> Result<()> { @@ -93,33 +93,39 @@ 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."); - std::process::exit(1); + which::which("cargo").context( + "Failed to find `cargo`. +Did you already install Rust? +Try running `cargo --version` to diagnose the problem.", + )?; + + let exercises = ExerciseList::parse()?.exercises; + + if matches!(args.command, Some(Subcommands::Init)) { + init::init_rustlings(&exercises).context("Initialization failed")?; + 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, run the command `rustlings init` to initialize it." + ); + 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(|| { println!("{DEFAULT_OUT}\n"); - std::process::exit(0); + exit(0); }); match command { + // `Init` is handled above. + Subcommands::Init => (), Subcommands::List { paths, names, @@ -152,7 +158,7 @@ fn main() -> Result<()> { 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" @@ -177,8 +183,8 @@ fn main() -> Result<()> { 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), }; }); } @@ -192,46 +198,37 @@ fn main() -> Result<()> { 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); - - reset(exercise).unwrap_or_else(|_| std::process::exit(1)); + let exercise = find_exercise(&name, &exercises)?; + 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()); } 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)); - } - - 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::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) => { 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!( @@ -298,25 +295,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 { @@ -324,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() { @@ -341,57 +332,49 @@ 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(); - let failed_exercise_hint = match verify( - exercises.iter(), - (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)); + + 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(); - 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) - })); - let num_done = exercises - .iter() - .filter(|e| e.looks_done() && !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()); + )? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => { + let hint = exercise.hint.clone(); + *failed_exercise_hint.lock().unwrap() = Some(hint); } } + + pending_exercises.clear(); } } } @@ -409,9 +392,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: @@ -431,11 +421,19 @@ 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, run `rustlings watch` in order to get the first -exercise. Make sure to have your editor open!"; +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 + 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! | @@ -463,21 +461,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 0f56de9..0000000 --- a/src/project.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Serialize; -use std::env; -use std::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 { - sysroot_src: PathBuf, - crates: Vec, -} - -#[derive(Serialize)] -struct Crate { - root_module: PathBuf, - 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 RustAnalyzerProject { - fn build(exercises: Vec) -> Result { - let crates = exercises - .into_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: Vec) -> 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(()) -} diff --git a/src/run.rs b/src/run.rs index 6dd0388..3f93f14 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,4 +1,5 @@ -use std::process::Command; +use anyhow::{bail, Result}; +use std::io::{stdout, Write}; use std::time::Duration; use crate::exercise::{Exercise, Mode}; @@ -9,67 +10,30 @@ 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)?, - } - Ok(()) -} - -// 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(()), + Mode::Test => test(exercise, verbose), + Mode::Compile | Mode::Clippy => compile_and_run(exercise), } } -// 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<(), ()> { +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/ui.rs b/src/ui.rs index d8177b9..22d60d9 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!( diff --git a/src/verify.rs b/src/verify.rs index dac2562..ef966f6 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,7 +1,19 @@ -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}; + +pub enum VerifyState<'a> { + AllExercisesDone, + Failed(&'a Exercise), +} // Verify that the provided container of Exercise objects // can be compiled and run without any failures. @@ -9,11 +21,11 @@ use std::{env, time::Duration}; // 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, -) -> 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; @@ -26,29 +38,24 @@ 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), - 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); bar.set_message(format!("({percentage:.1} %)")); - if bar.position() == total as u64 { - println!( - "Progress: You completed {} / {} exercises ({:.1} %).", - bar.position(), - total, - percentage - ); - bar.finish(); - } } - Ok(()) + + bar.finish(); + println!("You completed all exercises!"); + + Ok(VerifyState::AllExercisesDone) } #[derive(PartialEq, Eq)] @@ -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)) + 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, - )) + prompt_for_completion(exercise, Some(output), success_hints) } // Compile the given Exercise as a test harness and display @@ -111,66 +112,46 @@ 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 { + 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() { - State::Done => return true, +) -> Result { + let context = match exercise.state()? { + State::Done => return Ok(true), State::Pending(context) => context, }; match exercise.mode { @@ -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!( @@ -234,7 +215,7 @@ fn prompt_for_completion( ); } - false + Ok(false) } fn separator() -> console::StyledObject<&'static str> { diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs new file mode 100644 index 0000000..7f1771b --- /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; + } +} diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml new file mode 100644 index 0000000..e111cf2 --- /dev/null +++ b/tests/fixture/failure/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tests" +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/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 e5949f9..9474ee3 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/Cargo.toml b/tests/fixture/state/Cargo.toml new file mode 100644 index 0000000..c8d74e4 --- /dev/null +++ b/tests/fixture/state/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tests" +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/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 547b3a4..8de5d60 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/Cargo.toml b/tests/fixture/success/Cargo.toml new file mode 100644 index 0000000..f26a44f --- /dev/null +++ b/tests/fixture/success/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tests" +edition = "2021" +publish = false + +[[bin]] +name = "compSuccess" +path = "exercises/compSuccess.rs" + +[[bin]] +name = "testSuccess" +path = "exercises/testSuccess.rs" 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 68d3538..17ed8c6 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 = """"""