summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMo <76752051+mo8it@users.noreply.github.com>2024-04-04 15:48:07 +0200
committerGitHub <noreply@github.com>2024-04-04 15:48:07 +0200
commit8c8f30d8ce3b732de649938d8945496bd769ac22 (patch)
tree3679a78e468872f05cec1de4e489acbbc08a11a8
parent459c52137ac7b8aa8500a46f04b0e848ba48a969 (diff)
parentb6c434c445d91a9e886e5639b078635e5eca4eb3 (diff)
Merge pull request #1931 from mo8it/standalone-binary
Standalone binary
-rw-r--r--.gitignore8
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml36
-rw-r--r--dev/Cargo.toml106
-rw-r--r--install.ps194
-rwxr-xr-xinstall.sh184
-rw-r--r--rustlings-macros/Cargo.toml12
-rw-r--r--rustlings-macros/src/lib.rs95
-rw-r--r--src/bin/gen-dev-cargo-toml.rs64
-rw-r--r--src/embedded.rs117
-rw-r--r--src/exercise.rs299
-rw-r--r--src/init.rs97
-rw-r--r--src/main.rs254
-rw-r--r--src/project.rs83
-rw-r--r--src/run.rs70
-rw-r--r--src/ui.rs2
-rw-r--r--src/verify.rs167
-rw-r--r--tests/dev_cargo_bins.rs39
-rw-r--r--tests/fixture/failure/Cargo.toml20
-rw-r--r--tests/fixture/failure/exercises/compFailure.rs (renamed from tests/fixture/failure/compFailure.rs)0
-rw-r--r--tests/fixture/failure/exercises/compNoExercise.rs (renamed from tests/fixture/failure/compNoExercise.rs)0
-rw-r--r--tests/fixture/failure/exercises/testFailure.rs (renamed from tests/fixture/failure/testFailure.rs)0
-rw-r--r--tests/fixture/failure/exercises/testNotPassed.rs (renamed from tests/fixture/failure/testNotPassed.rs)0
-rw-r--r--tests/fixture/failure/info.toml4
-rw-r--r--tests/fixture/state/Cargo.toml16
-rw-r--r--tests/fixture/state/exercises/finished_exercise.rs (renamed from tests/fixture/state/finished_exercise.rs)0
-rw-r--r--tests/fixture/state/exercises/pending_exercise.rs (renamed from tests/fixture/state/pending_exercise.rs)0
-rw-r--r--tests/fixture/state/exercises/pending_test_exercise.rs (renamed from tests/fixture/state/pending_test_exercise.rs)0
-rw-r--r--tests/fixture/state/info.toml7
-rw-r--r--tests/fixture/success/Cargo.toml12
-rw-r--r--tests/fixture/success/exercises/compSuccess.rs (renamed from tests/fixture/success/compSuccess.rs)0
-rw-r--r--tests/fixture/success/exercises/testSuccess.rs (renamed from tests/fixture/success/testSuccess.rs)0
-rw-r--r--tests/fixture/success/info.toml4
33 files changed, 906 insertions, 894 deletions
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",
@@ -583,6 +584,13 @@ dependencies = [
]
[[package]]
+name = "rustlings-macros"
+version = "6.0.0"
+dependencies = [
+ "quote",
+]
+
+[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 <mokou@fastmail.com>",
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
]
+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<Exercise>,
+}
+
+fn main() -> Result<()> {
+ let exercises = toml_edit::de::from_str::<InfoToml>(
+ &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<P: AsRef<Path>>(self, path: P) -> io::Result<File> {
+ 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"];
+use crate::embedded::EMBEDDED_FILES;
+
+// The number of context lines above and below a highlighted line.
const CONTEXT: usize = 2;
-const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml";
-// Checks if the line contains the "I AM NOT DONE" comment.
+// 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<Exercise>,
}
-// A representation of a rustlings exercise.
-// This is deserialized from the accompanying info.toml file
-#[derive(Deserialize, Debug)]
+impl ExerciseList {
+ pub fn parse() -> Result<Self> {
+ // 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<ContextLine>),
}
-// 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<ExerciseOutput, ExerciseOutput> {
- 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<CompiledExercise, ExerciseOutput> {
- 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<Output> {
+ 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");
}
- }
- fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
- 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");
-
- let output = ExerciseOutput {
- stdout: String::from_utf8_lossy(&cmd.stdout).to_string(),
- stderr: String::from_utf8_lossy(&cmd.stderr).to_string(),
- };
+ .context("Failed to run Cargo")
+ }
- if cmd.status.success() {
- Ok(output)
- } else {
- Err(output)
+ pub fn run(&self) -> Result<Output> {
+ 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<State> {
+ 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<bool> {
+ 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 -- <filename>"
+ /// 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::<ExerciseList>(&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<WatchStatus> {
+fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<WatchStatus> {
/* 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
+ !<cmd> - 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
- !<cmd> - 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<Crate>,
-}
-
-#[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<Exercise>) -> Result<Self> {
- 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<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(())
-}
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<Item = &'a Exercise>,
+ pending_exercises: impl IntoIterator<Item = &'a Exercise>,
progress: (usize, usize),
verbose: bool,
success_hints: bool,
-) -> Result<(), &'a Exercise> {
+) -> Result<VerifyState<'a>> {
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<bool, ()> {
+fn compile_only(exercise: &Exercise, success_hints: bool) -> Result<bool> {
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<bool, ()> {
+fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result<bool> {
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<bool, ()> {
+) -> Result<bool> {
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<CompiledExercise<'a>, ()> {
- let compilation_result = exercise.compile();
-
- 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 verbose {
+ stdout().write_all(&output.stdout)?;
+ }
+
+ if run_mode == RunMode::Interactive {
+ prompt_for_completion(exercise, None, success_hints)
+ } else {
+ Ok(true)
}
}
fn prompt_for_completion(
exercise: &Exercise,
- prompt_output: Option<String>,
+ prompt_output: Option<Output>,
success_hints: bool,
-) -> bool {
- let context = match exercise.state() {
- State::Done => return true,
+) -> Result<bool> {
+ 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<Exercise>,
+}
+
+#[test]
+fn dev_cargo_bins() {
+ let content = fs::read_to_string("exercises/Cargo.toml").unwrap();
+
+ let exercises = toml_edit::de::from_str::<InfoToml>(&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
index 566856a..566856a 100644
--- a/tests/fixture/failure/compFailure.rs
+++ b/tests/fixture/failure/exercises/compFailure.rs
diff --git a/tests/fixture/failure/compNoExercise.rs b/tests/fixture/failure/exercises/compNoExercise.rs
index f79c691..f79c691 100644
--- a/tests/fixture/failure/compNoExercise.rs
+++ b/tests/fixture/failure/exercises/compNoExercise.rs
diff --git a/tests/fixture/failure/testFailure.rs b/tests/fixture/failure/exercises/testFailure.rs
index b33a5d2..b33a5d2 100644
--- a/tests/fixture/failure/testFailure.rs
+++ b/tests/fixture/failure/exercises/testFailure.rs
diff --git a/tests/fixture/failure/testNotPassed.rs b/tests/fixture/failure/exercises/testNotPassed.rs
index a9fe88d..a9fe88d 100644
--- a/tests/fixture/failure/testNotPassed.rs
+++ b/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
index 016b827..016b827 100644
--- a/tests/fixture/state/finished_exercise.rs
+++ b/tests/fixture/state/exercises/finished_exercise.rs
diff --git a/tests/fixture/state/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs
index f579d0b..f579d0b 100644
--- a/tests/fixture/state/pending_exercise.rs
+++ b/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
index 8756f02..8756f02 100644
--- a/tests/fixture/state/pending_test_exercise.rs
+++ b/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
index f79c691..f79c691 100644
--- a/tests/fixture/success/compSuccess.rs
+++ b/tests/fixture/success/exercises/compSuccess.rs
diff --git a/tests/fixture/success/testSuccess.rs b/tests/fixture/success/exercises/testSuccess.rs
index 7139b50..7139b50 100644
--- a/tests/fixture/success/testSuccess.rs
+++ b/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 = """"""