mirror of
https://github.com/rust-lang/rustlings.git
synced 2024-12-27 00:00:03 +03:00
Merge pull request #1931 from mo8it/standalone-binary
Standalone binary
This commit is contained in:
commit
8c8f30d8ce
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,11 +1,11 @@
|
||||||
*.swp
|
|
||||||
target/
|
target/
|
||||||
|
/tests/fixture/*/Cargo.lock
|
||||||
|
/dev/Cargo.lock
|
||||||
|
|
||||||
|
*.swp
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pdb
|
*.pdb
|
||||||
exercises/22_clippy/Cargo.toml
|
|
||||||
exercises/22_clippy/Cargo.lock
|
|
||||||
rust-project.json
|
|
||||||
.idea
|
.idea
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -564,7 +564,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustlings"
|
name = "rustlings"
|
||||||
version = "5.6.1"
|
version = "6.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
|
@ -574,6 +574,7 @@ dependencies = [
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
"predicates",
|
"predicates",
|
||||||
|
"rustlings-macros",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
@ -582,6 +583,13 @@ dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustlings-macros"
|
||||||
|
version = "6.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
|
|
36
Cargo.toml
36
Cargo.toml
|
@ -1,19 +1,37 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "rustlings"
|
resolver = "2"
|
||||||
description = "Small exercises to get you used to reading and writing Rust code!"
|
exclude = [
|
||||||
version = "5.6.1"
|
"tests/fixture/failure",
|
||||||
|
"tests/fixture/state",
|
||||||
|
"tests/fixture/success",
|
||||||
|
"dev",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "6.0.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Liv <mokou@fastmail.com>",
|
"Liv <mokou@fastmail.com>",
|
||||||
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
|
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
|
||||||
]
|
]
|
||||||
|
license = "MIT"
|
||||||
edition = "2021"
|
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]
|
[dependencies]
|
||||||
anyhow = "1.0.81"
|
anyhow = "1.0.81"
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
console = "0.15.8"
|
console = "0.15.8"
|
||||||
indicatif = "0.17.8"
|
indicatif = "0.17.8"
|
||||||
notify-debouncer-mini = "0.4.1"
|
notify-debouncer-mini = "0.4.1"
|
||||||
|
rustlings-macros = { path = "rustlings-macros" }
|
||||||
serde_json = "1.0.115"
|
serde_json = "1.0.115"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
|
@ -21,11 +39,13 @@ toml_edit = { version = "0.22.9", default-features = false, features = ["parse",
|
||||||
which = "6.0.1"
|
which = "6.0.1"
|
||||||
winnow = "0.6.5"
|
winnow = "0.6.5"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "rustlings"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0.14"
|
assert_cmd = "2.0.14"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
predicates = "3.1.0"
|
predicates = "3.1.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
panic = "abort"
|
||||||
|
|
106
dev/Cargo.toml
Normal file
106
dev/Cargo.toml
Normal file
|
@ -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
|
94
install.ps1
94
install.ps1
|
@ -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!"
|
|
184
install.sh
184
install.sh
|
@ -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."
|
|
12
rustlings-macros/Cargo.toml
Normal file
12
rustlings-macros/Cargo.toml
Normal file
|
@ -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"
|
95
rustlings-macros/src/lib.rs
Normal file
95
rustlings-macros/src/lib.rs
Normal file
|
@ -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()
|
||||||
|
}
|
64
src/bin/gen-dev-cargo-toml.rs
Normal file
64
src/bin/gen-dev-cargo-toml.rs
Normal file
|
@ -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`")
|
||||||
|
}
|
117
src/embedded.rs
Normal file
117
src/embedded.rs
Normal file
|
@ -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()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
303
src/exercise.rs
303
src/exercise.rs
|
@ -1,21 +1,21 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
use std::fs::{self, remove_file, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, BufRead, BufReader};
|
use std::io::{self, BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{self, exit, Command, Stdio};
|
use std::process::{exit, Command, Output};
|
||||||
use std::{array, env, mem};
|
use std::{array, mem};
|
||||||
use winnow::ascii::{space0, Caseless};
|
use winnow::ascii::{space0, Caseless};
|
||||||
use winnow::combinator::opt;
|
use winnow::combinator::opt;
|
||||||
use winnow::Parser;
|
use winnow::Parser;
|
||||||
|
|
||||||
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
|
use crate::embedded::EMBEDDED_FILES;
|
||||||
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";
|
|
||||||
|
|
||||||
// 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 {
|
fn contains_not_done_comment(input: &str) -> bool {
|
||||||
(
|
(
|
||||||
space0::<_, ()>,
|
space0::<_, ()>,
|
||||||
|
@ -28,26 +28,15 @@ fn contains_not_done_comment(input: &str) -> bool {
|
||||||
.is_ok()
|
.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.
|
// The mode of the exercise.
|
||||||
#[derive(Deserialize, Copy, Clone, Debug)]
|
#[derive(Deserialize, Copy, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
// Indicates that the exercise should be compiled as a binary
|
// The exercise should be compiled as a binary
|
||||||
Compile,
|
Compile,
|
||||||
// Indicates that the exercise should be compiled as a test harness
|
// The exercise should be compiled as a test harness
|
||||||
Test,
|
Test,
|
||||||
// Indicates that the exercise should be linted with clippy
|
// The exercise should be linted with clippy
|
||||||
Clippy,
|
Clippy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,182 +45,86 @@ pub struct ExerciseList {
|
||||||
pub exercises: Vec<Exercise>,
|
pub exercises: Vec<Exercise>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A representation of a rustlings exercise.
|
impl ExerciseList {
|
||||||
// This is deserialized from the accompanying info.toml file
|
pub fn parse() -> Result<Self> {
|
||||||
#[derive(Deserialize, Debug)]
|
// 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 {
|
pub struct Exercise {
|
||||||
// Name of the exercise
|
// Name of the exercise
|
||||||
pub name: String,
|
pub name: String,
|
||||||
// The path to the file containing the exercise's source code
|
// The path to the file containing the exercise's source code
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
// The mode of the exercise (Test, Compile, or Clippy)
|
// The mode of the exercise
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
// The hint text associated with the exercise
|
// The hint text associated with the exercise
|
||||||
pub hint: String,
|
pub hint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// An enum to track of the state of an Exercise.
|
// The state of an Exercise.
|
||||||
// An Exercise can be either Done or Pending
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
// The state of the exercise once it's been completed
|
|
||||||
Done,
|
Done,
|
||||||
// The state of the exercise while it's not completed yet
|
|
||||||
Pending(Vec<ContextLine>),
|
Pending(Vec<ContextLine>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// The context information of a pending exercise
|
// The context information of a pending exercise.
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub struct ContextLine {
|
pub struct ContextLine {
|
||||||
// The source code that is still pending completion
|
// The source code line
|
||||||
pub line: String,
|
pub line: String,
|
||||||
// The line number of the source code still pending completion
|
// The line number
|
||||||
pub number: usize,
|
pub number: usize,
|
||||||
// Whether or not this is important
|
// Whether this is important and should be highlighted
|
||||||
pub important: bool,
|
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 {
|
impl Exercise {
|
||||||
pub fn compile(&self) -> Result<CompiledExercise, ExerciseOutput> {
|
fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result<Output> {
|
||||||
let cmd = match self.mode {
|
let mut cmd = Command::new("cargo");
|
||||||
Mode::Compile => Command::new("rustc")
|
cmd.arg(command);
|
||||||
.args([self.path.to_str().unwrap(), "-o", &temp_file()])
|
|
||||||
.args(RUSTC_COLOR_ARGS)
|
// A hack to make `cargo run` work when developing Rustlings.
|
||||||
.args(RUSTC_EDITION_ARGS)
|
// Use `dev/Cargo.toml` when in the directory of the repository.
|
||||||
.args(RUSTC_NO_DEBUG_ARGS)
|
#[cfg(debug_assertions)]
|
||||||
.output(),
|
if std::path::Path::new("tests").exists() {
|
||||||
Mode::Test => Command::new("rustc")
|
cmd.arg("--manifest-path").arg("dev/Cargo.toml");
|
||||||
.args(["--test", self.path.to_str().unwrap(), "-o", &temp_file()])
|
}
|
||||||
.args(RUSTC_COLOR_ARGS)
|
|
||||||
.args(RUSTC_EDITION_ARGS)
|
cmd.arg("--color")
|
||||||
.args(RUSTC_NO_DEBUG_ARGS)
|
.arg("always")
|
||||||
.output(),
|
.arg("-q")
|
||||||
Mode::Clippy => {
|
.arg("--bin")
|
||||||
let cargo_toml = format!(
|
.arg(&self.name)
|
||||||
r#"[package]
|
.args(args)
|
||||||
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()
|
.output()
|
||||||
|
.context("Failed to run Cargo")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.expect("Failed to run 'compile' command.");
|
|
||||||
|
|
||||||
if cmd.status.success() {
|
pub fn run(&self) -> Result<Output> {
|
||||||
Ok(CompiledExercise {
|
match self.mode {
|
||||||
exercise: self,
|
Mode::Compile => self.cargo_cmd("run", &[]),
|
||||||
_handle: FileHandle,
|
Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]),
|
||||||
})
|
Mode::Clippy => self.cargo_cmd(
|
||||||
} else {
|
"clippy",
|
||||||
clean();
|
&["--", "-D", "warnings", "-D", "clippy::float_cmp"],
|
||||||
Err(ExerciseOutput {
|
),
|
||||||
stdout: String::from_utf8_lossy(&cmd.stdout).to_string(),
|
|
||||||
stderr: String::from_utf8_lossy(&cmd.stderr).to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
|
pub fn state(&self) -> Result<State> {
|
||||||
let arg = match self.mode {
|
let source_file = File::open(&self.path)
|
||||||
Mode::Test => "--show-output",
|
.with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?;
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
let cmd = Command::new(temp_file())
|
|
||||||
.arg(arg)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to run 'run' command");
|
|
||||||
|
|
||||||
let output = ExerciseOutput {
|
|
||||||
stdout: String::from_utf8_lossy(&cmd.stdout).to_string(),
|
|
||||||
stderr: String::from_utf8_lossy(&cmd.stderr).to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if cmd.status.success() {
|
|
||||||
Ok(output)
|
|
||||||
} else {
|
|
||||||
Err(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn 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);
|
|
||||||
});
|
|
||||||
let mut source_reader = BufReader::new(source_file);
|
let mut source_reader = BufReader::new(source_file);
|
||||||
|
|
||||||
// Read the next line into `buf` without the newline at the end.
|
// 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.
|
// Reached the end of the file and didn't find the comment.
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return State::Done;
|
return Ok(State::Done);
|
||||||
}
|
}
|
||||||
|
|
||||||
if contains_not_done_comment(&line) {
|
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;
|
current_line_number += 1;
|
||||||
|
@ -328,64 +221,26 @@ path = "{}.rs""#,
|
||||||
// without actually having solved anything.
|
// without actually having solved anything.
|
||||||
// The only other way to truly check this would to compile and run
|
// The only other way to truly check this would to compile and run
|
||||||
// the exercise; which would be both costly and counterintuitive
|
// the exercise; which would be both costly and counterintuitive
|
||||||
pub fn looks_done(&self) -> bool {
|
pub fn looks_done(&self) -> Result<bool> {
|
||||||
self.state() == State::Done
|
self.state().map(|state| state == State::Done)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Exercise {
|
impl Display for Exercise {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_pending_state() {
|
fn test_pending_state() {
|
||||||
let exercise = Exercise {
|
let exercise = Exercise {
|
||||||
name: "pending_exercise".into(),
|
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,
|
mode: Mode::Compile,
|
||||||
hint: String::new(),
|
hint: String::new(),
|
||||||
};
|
};
|
||||||
|
@ -419,31 +274,19 @@ mod test {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
assert_eq!(state, State::Pending(expected));
|
assert_eq!(state.unwrap(), State::Pending(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_finished_exercise() {
|
fn test_finished_exercise() {
|
||||||
let exercise = Exercise {
|
let exercise = Exercise {
|
||||||
name: "finished_exercise".into(),
|
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,
|
mode: Mode::Compile,
|
||||||
hint: String::new(),
|
hint: String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(exercise.state(), State::Done);
|
assert_eq!(exercise.state().unwrap(), State::Done);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_exercise_with_output() {
|
|
||||||
let exercise = Exercise {
|
|
||||||
name: "exercise_with_output".into(),
|
|
||||||
path: PathBuf::from("tests/fixture/success/testSuccess.rs"),
|
|
||||||
mode: Mode::Test,
|
|
||||||
hint: String::new(),
|
|
||||||
};
|
|
||||||
let out = exercise.compile().unwrap().run().unwrap();
|
|
||||||
assert!(out.stdout.contains("THIS TEST TOO SHALL PASS"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
97
src/init.rs
Normal file
97
src/init.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
242
src/main.rs
242
src/main.rs
|
@ -1,29 +1,29 @@
|
||||||
|
use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
|
||||||
use crate::exercise::{Exercise, ExerciseList};
|
use crate::exercise::{Exercise, ExerciseList};
|
||||||
use crate::project::write_project_json;
|
use crate::run::run;
|
||||||
use crate::run::{reset, run};
|
|
||||||
use crate::verify::verify;
|
use crate::verify::verify;
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use console::Emoji;
|
use console::Emoji;
|
||||||
use notify_debouncer_mini::notify::{self, RecursiveMode};
|
use notify_debouncer_mini::notify::RecursiveMode;
|
||||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||||
use shlex::Shlex;
|
use shlex::Shlex;
|
||||||
use std::ffi::OsStr;
|
use std::io::{BufRead, Write};
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, prelude::*};
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::{exit, Command};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::mpsc::{channel, RecvTimeoutError};
|
use std::sync::mpsc::{channel, RecvTimeoutError};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::{io, thread};
|
||||||
|
use verify::VerifyState;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
mod embedded;
|
||||||
mod exercise;
|
mod exercise;
|
||||||
mod project;
|
mod init;
|
||||||
mod run;
|
mod run;
|
||||||
mod verify;
|
mod verify;
|
||||||
|
|
||||||
|
@ -40,6 +40,8 @@ struct Args {
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Subcommands {
|
enum Subcommands {
|
||||||
|
/// Initialize Rustlings
|
||||||
|
Init,
|
||||||
/// Verify all exercises according to the recommended order
|
/// Verify all exercises according to the recommended order
|
||||||
Verify,
|
Verify,
|
||||||
/// Rerun `verify` when files were edited
|
/// Rerun `verify` when files were edited
|
||||||
|
@ -53,7 +55,7 @@ enum Subcommands {
|
||||||
/// The name of the exercise
|
/// The name of the exercise
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
/// Reset a single exercise using "git stash -- <filename>"
|
/// Reset a single exercise
|
||||||
Reset {
|
Reset {
|
||||||
/// The name of the exercise
|
/// The name of the exercise
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -82,8 +84,6 @@ enum Subcommands {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
solved: bool,
|
solved: bool,
|
||||||
},
|
},
|
||||||
/// Enable rust-analyzer for exercises
|
|
||||||
Lsp,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
@ -93,33 +93,39 @@ fn main() -> Result<()> {
|
||||||
println!("\n{WELCOME}\n");
|
println!("\n{WELCOME}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if which::which("rustc").is_err() {
|
which::which("cargo").context(
|
||||||
println!("We cannot find `rustc`.");
|
"Failed to find `cargo`.
|
||||||
println!("Try running `rustc --version` to diagnose your problem.");
|
Did you already install Rust?
|
||||||
println!("For instructions on how to install Rust, check the README.");
|
Try running `cargo --version` to diagnose the problem.",
|
||||||
std::process::exit(1);
|
)?;
|
||||||
|
|
||||||
|
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 verbose = args.nocapture;
|
||||||
|
|
||||||
let command = args.command.unwrap_or_else(|| {
|
let command = args.command.unwrap_or_else(|| {
|
||||||
println!("{DEFAULT_OUT}\n");
|
println!("{DEFAULT_OUT}\n");
|
||||||
std::process::exit(0);
|
exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
|
// `Init` is handled above.
|
||||||
|
Subcommands::Init => (),
|
||||||
Subcommands::List {
|
Subcommands::List {
|
||||||
paths,
|
paths,
|
||||||
names,
|
names,
|
||||||
|
@ -152,7 +158,7 @@ fn main() -> Result<()> {
|
||||||
let filter_cond = filters
|
let filter_cond = filters
|
||||||
.iter()
|
.iter()
|
||||||
.any(|f| exercise.name.contains(f) || fname.contains(f));
|
.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 {
|
let status = if looks_done {
|
||||||
exercises_done += 1;
|
exercises_done += 1;
|
||||||
"Done"
|
"Done"
|
||||||
|
@ -177,8 +183,8 @@ fn main() -> Result<()> {
|
||||||
let mut handle = stdout.lock();
|
let mut handle = stdout.lock();
|
||||||
handle.write_all(line.as_bytes()).unwrap_or_else(|e| {
|
handle.write_all(line.as_bytes()).unwrap_or_else(|e| {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
std::io::ErrorKind::BrokenPipe => std::process::exit(0),
|
std::io::ErrorKind::BrokenPipe => exit(0),
|
||||||
_ => std::process::exit(1),
|
_ => exit(1),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -192,46 +198,37 @@ fn main() -> Result<()> {
|
||||||
exercises.len(),
|
exercises.len(),
|
||||||
percentage_progress
|
percentage_progress
|
||||||
);
|
);
|
||||||
std::process::exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Subcommands::Run { name } => {
|
Subcommands::Run { name } => {
|
||||||
let exercise = find_exercise(&name, &exercises);
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
|
run(exercise, verbose).unwrap_or_else(|_| exit(1));
|
||||||
run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Subcommands::Reset { name } => {
|
Subcommands::Reset { name } => {
|
||||||
let exercise = find_exercise(&name, &exercises);
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
|
EMBEDDED_FILES
|
||||||
reset(exercise).unwrap_or_else(|_| std::process::exit(1));
|
.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 } => {
|
Subcommands::Hint { name } => {
|
||||||
let exercise = find_exercise(&name, &exercises);
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
|
|
||||||
println!("{}", exercise.hint);
|
println!("{}", exercise.hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
Subcommands::Verify => {
|
Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? {
|
||||||
verify(&exercises, (0, exercises.len()), verbose, false)
|
VerifyState::AllExercisesDone => println!("All exercises done!"),
|
||||||
.unwrap_or_else(|_| std::process::exit(1));
|
VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
|
||||||
}
|
},
|
||||||
|
|
||||||
Subcommands::Lsp => {
|
|
||||||
if let Err(e) = write_project_json(exercises) {
|
|
||||||
println!("Failed to write rust-project.json to disk for rust-analyzer: {e}");
|
|
||||||
} else {
|
|
||||||
println!("Successfully generated rust-project.json");
|
|
||||||
println!("rust-analyzer will now parse exercises, restart your language server or editor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) {
|
Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error: Could not watch your progress. Error message was {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.");
|
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) => {
|
Ok(WatchStatus::Finished) => {
|
||||||
println!(
|
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" {
|
if name == "next" {
|
||||||
exercises
|
for exercise in exercises {
|
||||||
.iter()
|
if !exercise.looks_done()? {
|
||||||
.find(|e| !e.looks_done())
|
return Ok(exercise);
|
||||||
.unwrap_or_else(|| {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!("🎉 Congratulations! You have done all the exercises!");
|
println!("🎉 Congratulations! You have done all the exercises!");
|
||||||
println!("🔚 There are no more exercises to do next!");
|
println!("🔚 There are no more exercises to do next!");
|
||||||
std::process::exit(1)
|
exit(0);
|
||||||
})
|
}
|
||||||
} else {
|
|
||||||
exercises
|
exercises
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.name == name)
|
.find(|e| e.name == name)
|
||||||
.unwrap_or_else(|| {
|
.with_context(|| format!("No exercise found for '{name}'!"))
|
||||||
println!("No exercise found for '{name}'!");
|
|
||||||
std::process::exit(1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WatchStatus {
|
enum WatchStatus {
|
||||||
|
@ -324,11 +319,7 @@ enum WatchStatus {
|
||||||
Unfinished,
|
Unfinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(
|
fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<WatchStatus> {
|
||||||
exercises: &[Exercise],
|
|
||||||
verbose: bool,
|
|
||||||
success_hints: bool,
|
|
||||||
) -> notify::Result<WatchStatus> {
|
|
||||||
/* Clears the terminal with an ANSI escape code.
|
/* Clears the terminal with an ANSI escape code.
|
||||||
Works in UNIX and newer Windows terminals. */
|
Works in UNIX and newer Windows terminals. */
|
||||||
fn clear_screen() {
|
fn clear_screen() {
|
||||||
|
@ -341,57 +332,49 @@ fn watch(
|
||||||
let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
|
let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
|
||||||
debouncer
|
debouncer
|
||||||
.watcher()
|
.watcher()
|
||||||
.watch(Path::new("./exercises"), RecursiveMode::Recursive)?;
|
.watch(Path::new("exercises"), RecursiveMode::Recursive)?;
|
||||||
|
|
||||||
clear_screen();
|
clear_screen();
|
||||||
|
|
||||||
let failed_exercise_hint = match verify(
|
let failed_exercise_hint =
|
||||||
exercises.iter(),
|
match verify(exercises, (0, exercises.len()), verbose, success_hints)? {
|
||||||
(0, exercises.len()),
|
VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
|
||||||
verbose,
|
VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
|
||||||
success_hints,
|
|
||||||
) {
|
|
||||||
Ok(_) => return Ok(WatchStatus::Finished),
|
|
||||||
Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
|
spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
|
||||||
|
|
||||||
|
let mut pending_exercises = Vec::with_capacity(exercises.len());
|
||||||
loop {
|
loop {
|
||||||
match rx.recv_timeout(Duration::from_secs(1)) {
|
match rx.recv_timeout(Duration::from_secs(1)) {
|
||||||
Ok(event) => match event {
|
Ok(event) => match event {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
for event in events {
|
for event in events {
|
||||||
let event_path = event.path;
|
|
||||||
if event.kind == DebouncedEventKind::Any
|
if event.kind == DebouncedEventKind::Any
|
||||||
&& event_path.extension() == Some(OsStr::new("rs"))
|
&& event.path.extension().is_some_and(|ext| ext == "rs")
|
||||||
&& event_path.exists()
|
|
||||||
{
|
{
|
||||||
let filepath = event_path.as_path().canonicalize().unwrap();
|
pending_exercises.extend(exercises.iter().filter(|exercise| {
|
||||||
let pending_exercises =
|
!exercise.looks_done().unwrap_or(false)
|
||||||
exercises
|
|| event.path.ends_with(&exercise.path)
|
||||||
.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
|
let num_done = exercises.len() - pending_exercises.len();
|
||||||
.iter()
|
|
||||||
.filter(|e| e.looks_done() && !filepath.ends_with(&e.path))
|
|
||||||
.count();
|
|
||||||
clear_screen();
|
clear_screen();
|
||||||
|
|
||||||
match verify(
|
match verify(
|
||||||
pending_exercises,
|
pending_exercises.iter().copied(),
|
||||||
(num_done, exercises.len()),
|
(num_done, exercises.len()),
|
||||||
verbose,
|
verbose,
|
||||||
success_hints,
|
success_hints,
|
||||||
) {
|
)? {
|
||||||
Ok(_) => return Ok(WatchStatus::Finished),
|
VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
|
||||||
Err(exercise) => {
|
VerifyState::Failed(exercise) => {
|
||||||
let mut failed_exercise_hint =
|
let hint = exercise.hint.clone();
|
||||||
failed_exercise_hint.lock().unwrap();
|
*failed_exercise_hint.lock().unwrap() = Some(hint);
|
||||||
*failed_exercise_hint = Some(exercise.hint.clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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:
|
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!
|
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,
|
(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!
|
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
|
Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
|
||||||
exercise. Make sure to have your editor open!";
|
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 = "+----------------------------------------------------+
|
const FENISH_LINE: &str = "+----------------------------------------------------+
|
||||||
| You made it to the Fe-nish line! |
|
| 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:
|
Before reporting an issue or contributing, please read our guidelines:
|
||||||
https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md";
|
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.";
|
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
66
src/run.rs
66
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 std::time::Duration;
|
||||||
|
|
||||||
use crate::exercise::{Exercise, Mode};
|
use crate::exercise::{Exercise, Mode};
|
||||||
|
@ -9,67 +10,30 @@ use indicatif::ProgressBar;
|
||||||
// and run the ensuing binary.
|
// and run the ensuing binary.
|
||||||
// The verbose argument helps determine whether or not to show
|
// The verbose argument helps determine whether or not to show
|
||||||
// the output from the test harnesses (if the mode of the exercise is test)
|
// 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 {
|
match exercise.mode {
|
||||||
Mode::Test => test(exercise, verbose)?,
|
Mode::Test => test(exercise, verbose),
|
||||||
Mode::Compile => compile_and_run(exercise)?,
|
Mode::Compile | Mode::Clippy => 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(()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the rust compiler on the path of the given exercise
|
// Compile and run an exercise.
|
||||||
// and run the ensuing binary.
|
|
||||||
// This is strictly for non-test binaries, so output is displayed
|
// This is strictly for non-test binaries, so output is displayed
|
||||||
fn compile_and_run(exercise: &Exercise) -> Result<(), ()> {
|
fn compile_and_run(exercise: &Exercise) -> Result<()> {
|
||||||
let progress_bar = ProgressBar::new_spinner();
|
let progress_bar = ProgressBar::new_spinner();
|
||||||
progress_bar.set_message(format!("Compiling {exercise}..."));
|
progress_bar.set_message(format!("Running {exercise}..."));
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
|
||||||
let compilation_result = exercise.compile();
|
let output = exercise.run()?;
|
||||||
let compilation = match compilation_result {
|
|
||||||
Ok(compilation) => compilation,
|
|
||||||
Err(output) => {
|
|
||||||
progress_bar.finish_and_clear();
|
progress_bar.finish_and_clear();
|
||||||
warn!(
|
|
||||||
"Compilation of {} failed!, Compiler error message:\n",
|
stdout().write_all(&output.stdout)?;
|
||||||
exercise
|
if !output.status.success() {
|
||||||
);
|
stdout().write_all(&output.stderr)?;
|
||||||
println!("{}", output.stderr);
|
warn!("Ran {} with errors", exercise);
|
||||||
return Err(());
|
bail!("TODO");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
progress_bar.set_message(format!("Running {exercise}..."));
|
|
||||||
let result = compilation.run();
|
|
||||||
progress_bar.finish_and_clear();
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(output) => {
|
|
||||||
println!("{}", output.stdout);
|
|
||||||
success!("Successfully ran {}", exercise);
|
success!("Successfully ran {}", exercise);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
Err(output) => {
|
|
||||||
println!("{}", output.stdout);
|
|
||||||
println!("{}", output.stderr);
|
|
||||||
|
|
||||||
warn!("Ran {} with errors", exercise);
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ macro_rules! print_emoji {
|
||||||
use console::{style, Emoji};
|
use console::{style, Emoji};
|
||||||
use std::env;
|
use std::env;
|
||||||
let formatstr = format!($fmt, $ex);
|
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());
|
println!("{} {}", style($sign).$color(), style(formatstr).$color());
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
|
|
155
src/verify.rs
155
src/verify.rs
|
@ -1,7 +1,19 @@
|
||||||
use crate::exercise::{CompiledExercise, Exercise, Mode, State};
|
use anyhow::{bail, Result};
|
||||||
use console::style;
|
use console::style;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
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
|
// Verify that the provided container of Exercise objects
|
||||||
// can be compiled and run without any failures.
|
// 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
|
// If the Exercise being verified is a test, the verbose boolean
|
||||||
// determines whether or not the test harness outputs are displayed.
|
// determines whether or not the test harness outputs are displayed.
|
||||||
pub fn verify<'a>(
|
pub fn verify<'a>(
|
||||||
exercises: impl IntoIterator<Item = &'a Exercise>,
|
pending_exercises: impl IntoIterator<Item = &'a Exercise>,
|
||||||
progress: (usize, usize),
|
progress: (usize, usize),
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
success_hints: bool,
|
success_hints: bool,
|
||||||
) -> Result<(), &'a Exercise> {
|
) -> Result<VerifyState<'a>> {
|
||||||
let (num_done, total) = progress;
|
let (num_done, total) = progress;
|
||||||
let bar = ProgressBar::new(total as u64);
|
let bar = ProgressBar::new(total as u64);
|
||||||
let mut percentage = num_done as f32 / total as f32 * 100.0;
|
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_position(num_done as u64);
|
||||||
bar.set_message(format!("({percentage:.1} %)"));
|
bar.set_message(format!("({percentage:.1} %)"));
|
||||||
|
|
||||||
for exercise in exercises {
|
for exercise in pending_exercises {
|
||||||
let compile_result = match exercise.mode {
|
let compile_result = match exercise.mode {
|
||||||
Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints),
|
Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?,
|
||||||
Mode::Compile => compile_and_run_interactively(exercise, success_hints),
|
Mode::Compile => compile_and_run_interactively(exercise, success_hints)?,
|
||||||
Mode::Clippy => compile_only(exercise, success_hints),
|
Mode::Clippy => compile_only(exercise, success_hints)?,
|
||||||
};
|
};
|
||||||
if !compile_result.unwrap_or(false) {
|
if !compile_result {
|
||||||
return Err(exercise);
|
return Ok(VerifyState::Failed(exercise));
|
||||||
}
|
}
|
||||||
percentage += 100.0 / total as f32;
|
percentage += 100.0 / total as f32;
|
||||||
bar.inc(1);
|
bar.inc(1);
|
||||||
bar.set_message(format!("({percentage:.1} %)"));
|
bar.set_message(format!("({percentage:.1} %)"));
|
||||||
if bar.position() == total as u64 {
|
}
|
||||||
println!(
|
|
||||||
"Progress: You completed {} / {} exercises ({:.1} %).",
|
|
||||||
bar.position(),
|
|
||||||
total,
|
|
||||||
percentage
|
|
||||||
);
|
|
||||||
bar.finish();
|
bar.finish();
|
||||||
}
|
println!("You completed all exercises!");
|
||||||
}
|
|
||||||
Ok(())
|
Ok(VerifyState::AllExercisesDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
|
@ -58,50 +65,44 @@ enum RunMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile and run the resulting test harness of the given Exercise
|
// 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)?;
|
compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the rust compiler without running the resulting binary
|
// 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();
|
let progress_bar = ProgressBar::new_spinner();
|
||||||
progress_bar.set_message(format!("Compiling {exercise}..."));
|
progress_bar.set_message(format!("Compiling {exercise}..."));
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
|
||||||
let _ = compile(exercise, &progress_bar)?;
|
let _ = exercise.run()?;
|
||||||
progress_bar.finish_and_clear();
|
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
|
// 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();
|
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));
|
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
|
||||||
let compilation = compile(exercise, &progress_bar)?;
|
let output = exercise.run()?;
|
||||||
|
|
||||||
progress_bar.set_message(format!("Running {exercise}..."));
|
|
||||||
let result = compilation.run();
|
|
||||||
progress_bar.finish_and_clear();
|
progress_bar.finish_and_clear();
|
||||||
|
|
||||||
let output = match result {
|
if !output.status.success() {
|
||||||
Ok(output) => output,
|
|
||||||
Err(output) => {
|
|
||||||
warn!("Ran {} with errors", exercise);
|
warn!("Ran {} with errors", exercise);
|
||||||
println!("{}", output.stdout);
|
{
|
||||||
println!("{}", output.stderr);
|
let mut stdout = stdout().lock();
|
||||||
return Err(());
|
stdout.write_all(&output.stdout)?;
|
||||||
|
stdout.write_all(&output.stderr)?;
|
||||||
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
bail!("TODO");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Ok(prompt_for_completion(
|
prompt_for_completion(exercise, Some(output), success_hints)
|
||||||
exercise,
|
|
||||||
Some(output.stdout),
|
|
||||||
success_hints,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile the given Exercise as a test harness and display
|
// Compile the given Exercise as a test harness and display
|
||||||
|
@ -111,66 +112,46 @@ fn compile_and_test(
|
||||||
run_mode: RunMode,
|
run_mode: RunMode,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
success_hints: bool,
|
success_hints: bool,
|
||||||
) -> Result<bool, ()> {
|
) -> Result<bool> {
|
||||||
let progress_bar = ProgressBar::new_spinner();
|
let progress_bar = ProgressBar::new_spinner();
|
||||||
progress_bar.set_message(format!("Testing {exercise}..."));
|
progress_bar.set_message(format!("Testing {exercise}..."));
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
|
||||||
let compilation = compile(exercise, &progress_bar)?;
|
let output = exercise.run()?;
|
||||||
let result = compilation.run();
|
|
||||||
progress_bar.finish_and_clear();
|
progress_bar.finish_and_clear();
|
||||||
|
|
||||||
match result {
|
if !output.status.success() {
|
||||||
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!(
|
warn!(
|
||||||
"Testing of {} failed! Please try again. Here's the output:",
|
"Testing of {} failed! Please try again. Here's the output:",
|
||||||
exercise
|
exercise
|
||||||
);
|
);
|
||||||
println!("{}", output.stdout);
|
{
|
||||||
Err(())
|
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
|
if verbose {
|
||||||
// about the state of the compilation
|
stdout().write_all(&output.stdout)?;
|
||||||
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 run_mode == RunMode::Interactive {
|
||||||
|
prompt_for_completion(exercise, None, success_hints)
|
||||||
|
} else {
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_completion(
|
fn prompt_for_completion(
|
||||||
exercise: &Exercise,
|
exercise: &Exercise,
|
||||||
prompt_output: Option<String>,
|
prompt_output: Option<Output>,
|
||||||
success_hints: bool,
|
success_hints: bool,
|
||||||
) -> bool {
|
) -> Result<bool> {
|
||||||
let context = match exercise.state() {
|
let context = match exercise.state()? {
|
||||||
State::Done => return true,
|
State::Done => return Ok(true),
|
||||||
State::Pending(context) => context,
|
State::Pending(context) => context,
|
||||||
};
|
};
|
||||||
match exercise.mode {
|
match exercise.mode {
|
||||||
|
@ -200,10 +181,10 @@ fn prompt_for_completion(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(output) = prompt_output {
|
if let Some(output) = prompt_output {
|
||||||
println!(
|
let separator = separator();
|
||||||
"Output:\n{separator}\n{output}\n{separator}\n",
|
println!("Output:\n{separator}");
|
||||||
separator = separator(),
|
stdout().write_all(&output.stdout).unwrap();
|
||||||
);
|
println!("\n{separator}\n");
|
||||||
}
|
}
|
||||||
if success_hints {
|
if success_hints {
|
||||||
println!(
|
println!(
|
||||||
|
@ -234,7 +215,7 @@ fn prompt_for_completion(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn separator() -> console::StyledObject<&'static str> {
|
fn separator() -> console::StyledObject<&'static str> {
|
||||||
|
|
39
tests/dev_cargo_bins.rs
Normal file
39
tests/dev_cargo_bins.rs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
20
tests/fixture/failure/Cargo.toml
Normal file
20
tests/fixture/failure/Cargo.toml
Normal file
|
@ -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"
|
|
@ -1,11 +1,11 @@
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "compFailure"
|
name = "compFailure"
|
||||||
path = "compFailure.rs"
|
path = "exercises/compFailure.rs"
|
||||||
mode = "compile"
|
mode = "compile"
|
||||||
hint = ""
|
hint = ""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "testFailure"
|
name = "testFailure"
|
||||||
path = "testFailure.rs"
|
path = "exercises/testFailure.rs"
|
||||||
mode = "test"
|
mode = "test"
|
||||||
hint = "Hello!"
|
hint = "Hello!"
|
||||||
|
|
16
tests/fixture/state/Cargo.toml
Normal file
16
tests/fixture/state/Cargo.toml
Normal file
|
@ -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"
|
|
@ -1,18 +1,17 @@
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "pending_exercise"
|
name = "pending_exercise"
|
||||||
path = "pending_exercise.rs"
|
path = "exercises/pending_exercise.rs"
|
||||||
mode = "compile"
|
mode = "compile"
|
||||||
hint = """"""
|
hint = """"""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "pending_test_exercise"
|
name = "pending_test_exercise"
|
||||||
path = "pending_test_exercise.rs"
|
path = "exercises/pending_test_exercise.rs"
|
||||||
mode = "test"
|
mode = "test"
|
||||||
hint = """"""
|
hint = """"""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "finished_exercise"
|
name = "finished_exercise"
|
||||||
path = "finished_exercise.rs"
|
path = "exercises/finished_exercise.rs"
|
||||||
mode = "compile"
|
mode = "compile"
|
||||||
hint = """"""
|
hint = """"""
|
||||||
|
|
||||||
|
|
12
tests/fixture/success/Cargo.toml
Normal file
12
tests/fixture/success/Cargo.toml
Normal file
|
@ -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"
|
|
@ -1,11 +1,11 @@
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "compSuccess"
|
name = "compSuccess"
|
||||||
path = "compSuccess.rs"
|
path = "exercises/compSuccess.rs"
|
||||||
mode = "compile"
|
mode = "compile"
|
||||||
hint = """"""
|
hint = """"""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "testSuccess"
|
name = "testSuccess"
|
||||||
path = "testSuccess.rs"
|
path = "exercises/testSuccess.rs"
|
||||||
mode = "test"
|
mode = "test"
|
||||||
hint = """"""
|
hint = """"""
|
||||||
|
|
Loading…
Reference in a new issue