Merge pull request #1931 from mo8it/standalone-binary

Standalone binary
This commit is contained in:
Mo 2024-04-04 15:48:07 +02:00 committed by GitHub
commit 8c8f30d8ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 905 additions and 893 deletions

8
.gitignore vendored
View file

@ -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
View file

@ -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"

View file

@ -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
View 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

View file

@ -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!"

View file

@ -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."

View 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"

View 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()
}

View 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
View 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()),
))
}
}

View file

@ -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
View 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(())
}

View file

@ -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.";

View file

@ -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(())
}

View file

@ -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(())
}
}
} }

View file

@ -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!(

View file

@ -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
View 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;
}
}

View 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"

View file

@ -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!"

View 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"

View file

@ -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 = """"""

View 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"

View file

@ -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 = """"""