Compare commits

..

12 commits

Author SHA1 Message Date
mo8it 7526c6b1f9 Update POST_INIT_MSG 2024-04-14 17:11:27 +02:00
mo8it 1cbabc3d28 Add the manual-run option 2024-04-14 17:10:53 +02:00
mo8it bd10b154fe Clear the terminal after showing the welcome message 2024-04-14 16:07:17 +02:00
mo8it 070a780d7f Trim the final message 2024-04-14 16:04:05 +02:00
mo8it 8aef915ee7 Show the welcome message 2024-04-14 16:03:49 +02:00
mo8it 3da860927d Use push instead of extend_from_slice on chars 2024-04-14 14:53:32 +02:00
mo8it 1c90575b9f Update deps 2024-04-14 05:13:50 +02:00
mo8it 9dcc4b7df5 Simplify the state file 2024-04-14 05:13:27 +02:00
mo8it 9831cbb139 Fix tests 2024-04-14 03:13:33 +02:00
mo8it bee62c89de Add terminal links 2024-04-14 02:41:19 +02:00
mo8it 5c0073a948 Tolerate changes in the state file 2024-04-14 01:15:43 +02:00
mo8it 2a26dfcb00 Remove unused ContextLine 2024-04-13 15:30:35 +02:00
23 changed files with 655 additions and 478 deletions

2
.gitignore vendored
View file

@ -4,7 +4,7 @@ target/
/dev/Cargo.lock /dev/Cargo.lock
# State file # State file
.rustlings-state.json .rustlings-state.txt
# oranda # oranda
public/ public/

72
Cargo.lock generated
View file

@ -261,9 +261,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
@ -684,12 +684,12 @@ dependencies = [
"assert_cmd", "assert_cmd",
"clap", "clap",
"crossterm", "crossterm",
"hashbrown",
"notify-debouncer-mini", "notify-debouncer-mini",
"predicates", "predicates",
"ratatui", "ratatui",
"rustlings-macros", "rustlings-macros",
"serde", "serde",
"serde_json",
"toml_edit", "toml_edit",
"which", "which",
] ]
@ -748,17 +748,6 @@ dependencies = [
"syn 2.0.58", "syn 2.0.58",
] ]
[[package]]
name = "serde_json"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.5" version = "0.6.5"
@ -1011,7 +1000,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [ dependencies = [
"windows-targets 0.52.4", "windows-targets 0.52.5",
] ]
[[package]] [[package]]
@ -1031,17 +1020,18 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.52.4", "windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.4", "windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.4", "windows_i686_gnu 0.52.5",
"windows_i686_msvc 0.52.4", "windows_i686_gnullvm",
"windows_x86_64_gnu 0.52.4", "windows_i686_msvc 0.52.5",
"windows_x86_64_gnullvm 0.52.4", "windows_x86_64_gnu 0.52.5",
"windows_x86_64_msvc 0.52.4", "windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
] ]
[[package]] [[package]]
@ -1052,9 +1042,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
@ -1064,9 +1054,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
@ -1076,9 +1066,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
@ -1088,9 +1084,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
@ -1100,9 +1096,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
@ -1112,9 +1108,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
@ -1124,9 +1120,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winnow" name = "winnow"

View file

@ -37,10 +37,10 @@ edition.workspace = true
anyhow.workspace = true anyhow.workspace = true
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"
hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
ratatui = "0.26.1" ratatui = "0.26.1"
rustlings-macros = { path = "rustlings-macros" } rustlings-macros = { path = "rustlings-macros" }
serde_json = "1.0.115"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
which = "6.0.1" which = "6.0.1"

View file

@ -1,6 +1,5 @@
// intro1.rs // intro1.rs
// //
// TODO: Update comment
// We sometimes encourage you to keep trying things on a given exercise, even // We sometimes encourage you to keep trying things on a given exercise, even
// after you already figured it out. If you got everything working and feel // after you already figured it out. If you got everything working and feel
// ready for the next exercise, remove the `I AM NOT DONE` comment below. // ready for the next exercise, remove the `I AM NOT DONE` comment below.

View file

@ -10,18 +10,18 @@ use std::{
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct Exercise { struct ExerciseInfo {
name: String, name: String,
path: String, dir: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct InfoToml { struct InfoFile {
exercises: Vec<Exercise>, exercises: Vec<ExerciseInfo>,
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let exercises = toml_edit::de::from_str::<InfoToml>( let exercise_infos = toml_edit::de::from_str::<InfoFile>(
&fs::read_to_string("info.toml").context("Failed to read `info.toml`")?, &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?,
) )
.context("Failed to deserialize `info.toml`")? .context("Failed to deserialize `info.toml`")?
@ -36,12 +36,16 @@ fn main() -> Result<()> {
bin = [\n", bin = [\n",
); );
for exercise in exercises { for exercise_info in exercise_infos {
buf.extend_from_slice(b" { name = \""); buf.extend_from_slice(b" { name = \"");
buf.extend_from_slice(exercise.name.as_bytes()); buf.extend_from_slice(exercise_info.name.as_bytes());
buf.extend_from_slice(b"\", path = \"../"); buf.extend_from_slice(b"\", path = \"../exercises/");
buf.extend_from_slice(exercise.path.as_bytes()); if let Some(dir) = &exercise_info.dir {
buf.extend_from_slice(b"\" },\n"); buf.extend_from_slice(dir.as_bytes());
buf.push(b'/');
}
buf.extend_from_slice(exercise_info.name.as_bytes());
buf.extend_from_slice(b".rs\" },\n");
} }
buf.extend_from_slice( buf.extend_from_slice(

272
info.toml
View file

@ -33,10 +33,11 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md
# INTRO # INTRO
# TODO: Update exercise
[[exercises]] [[exercises]]
name = "intro1" name = "intro1"
path = "exercises/00_intro/intro1.rs" dir = "00_intro"
mode = "compile" mode = "run"
# TODO: Fix hint # TODO: Fix hint
hint = """ hint = """
Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file
@ -44,8 +45,8 @@ to move on to the next exercise."""
[[exercises]] [[exercises]]
name = "intro2" name = "intro2"
path = "exercises/00_intro/intro2.rs" dir = "00_intro"
mode = "compile" mode = "run"
hint = """ hint = """
The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative."""
@ -53,16 +54,16 @@ The compiler is informing us that we've got the name of the print macro wrong, a
[[exercises]] [[exercises]]
name = "variables1" name = "variables1"
path = "exercises/01_variables/variables1.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
The declaration in the first line in the main function is missing a keyword The declaration in the first line in the main function is missing a keyword
that is needed in Rust to create a new variable binding.""" that is needed in Rust to create a new variable binding."""
[[exercises]] [[exercises]]
name = "variables2" name = "variables2"
path = "exercises/01_variables/variables2.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
The compiler message is saying that Rust cannot infer the type that the The compiler message is saying that Rust cannot infer the type that the
variable binding `x` has with what is given here. variable binding `x` has with what is given here.
@ -80,8 +81,8 @@ What if `x` is the same type as `10`? What if it's a different type?"""
[[exercises]] [[exercises]]
name = "variables3" name = "variables3"
path = "exercises/01_variables/variables3.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
Oops! In this exercise, we have a variable binding that we've created on in the Oops! In this exercise, we have a variable binding that we've created on in the
first line in the `main` function, and we're trying to use it in the next line, first line in the `main` function, and we're trying to use it in the next line,
@ -94,8 +95,8 @@ programming language -- thankfully the Rust compiler has caught this for us!"""
[[exercises]] [[exercises]]
name = "variables4" name = "variables4"
path = "exercises/01_variables/variables4.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
In Rust, variable bindings are immutable by default. But here we're trying In Rust, variable bindings are immutable by default. But here we're trying
to reassign a different value to `x`! There's a keyword we can use to make to reassign a different value to `x`! There's a keyword we can use to make
@ -103,8 +104,8 @@ a variable binding mutable instead."""
[[exercises]] [[exercises]]
name = "variables5" name = "variables5"
path = "exercises/01_variables/variables5.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
In `variables4` we already learned how to make an immutable variable mutable In `variables4` we already learned how to make an immutable variable mutable
using a special keyword. Unfortunately this doesn't help us much in this using a special keyword. Unfortunately this doesn't help us much in this
@ -121,8 +122,8 @@ Try to solve this exercise afterwards using this technique."""
[[exercises]] [[exercises]]
name = "variables6" name = "variables6"
path = "exercises/01_variables/variables6.rs" dir = "01_variables"
mode = "compile" mode = "run"
hint = """ hint = """
We know about variables and mutability, but there is another important type of We know about variables and mutability, but there is another important type of
variable available: constants. variable available: constants.
@ -141,8 +142,8 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants
[[exercises]] [[exercises]]
name = "functions1" name = "functions1"
path = "exercises/02_functions/functions1.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
This main function is calling a function that it expects to exist, but the This main function is calling a function that it expects to exist, but the
function doesn't exist. It expects this function to have the name `call_me`. function doesn't exist. It expects this function to have the name `call_me`.
@ -151,24 +152,24 @@ Sounds a lot like `main`, doesn't it?"""
[[exercises]] [[exercises]]
name = "functions2" name = "functions2"
path = "exercises/02_functions/functions2.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
Rust requires that all parts of a function's signature have type annotations, Rust requires that all parts of a function's signature have type annotations,
but `call_me` is missing the type annotation of `num`.""" but `call_me` is missing the type annotation of `num`."""
[[exercises]] [[exercises]]
name = "functions3" name = "functions3"
path = "exercises/02_functions/functions3.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
This time, the function *declaration* is okay, but there's something wrong This time, the function *declaration* is okay, but there's something wrong
with the place where we're calling the function.""" with the place where we're calling the function."""
[[exercises]] [[exercises]]
name = "functions4" name = "functions4"
path = "exercises/02_functions/functions4.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
The error message points to the function `sale_price` and says it expects a type The error message points to the function `sale_price` and says it expects a type
after the `->`. This is where the function's return type should be -- take a after the `->`. This is where the function's return type should be -- take a
@ -179,8 +180,8 @@ for the inputs of the functions here, since the original prices shouldn't be neg
[[exercises]] [[exercises]]
name = "functions5" name = "functions5"
path = "exercises/02_functions/functions5.rs" dir = "02_functions"
mode = "compile" mode = "run"
hint = """ hint = """
This is a really common error that can be fixed by removing one character. This is a really common error that can be fixed by removing one character.
It happens because Rust distinguishes between expressions and statements: It happens because Rust distinguishes between expressions and statements:
@ -198,7 +199,7 @@ They are not the same. There are two solutions:
[[exercises]] [[exercises]]
name = "if1" name = "if1"
path = "exercises/03_if/if1.rs" dir = "03_if"
mode = "test" mode = "test"
hint = """ hint = """
It's possible to do this in one line if you would like! It's possible to do this in one line if you would like!
@ -214,7 +215,7 @@ Remember in Rust that:
[[exercises]] [[exercises]]
name = "if2" name = "if2"
path = "exercises/03_if/if2.rs" dir = "03_if"
mode = "test" mode = "test"
hint = """ hint = """
For that first compiler error, it's important in Rust that each conditional For that first compiler error, it's important in Rust that each conditional
@ -223,7 +224,7 @@ conditions checking different input values."""
[[exercises]] [[exercises]]
name = "if3" name = "if3"
path = "exercises/03_if/if3.rs" dir = "03_if"
mode = "test" mode = "test"
hint = """ hint = """
In Rust, every arm of an `if` expression has to return the same type of value. In Rust, every arm of an `if` expression has to return the same type of value.
@ -233,7 +234,6 @@ Make sure the type is consistent across all arms."""
[[exercises]] [[exercises]]
name = "quiz1" name = "quiz1"
path = "exercises/quiz1.rs"
mode = "test" mode = "test"
hint = "No hints this time ;)" hint = "No hints this time ;)"
@ -241,20 +241,20 @@ hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "primitive_types1" name = "primitive_types1"
path = "exercises/04_primitive_types/primitive_types1.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "primitive_types2" name = "primitive_types2"
path = "exercises/04_primitive_types/primitive_types2.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "primitive_types3" name = "primitive_types3"
path = "exercises/04_primitive_types/primitive_types3.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = """ hint = """
There's a shorthand to initialize Arrays with a certain size that does not There's a shorthand to initialize Arrays with a certain size that does not
require you to type in 100 items (but you certainly can if you want!). require you to type in 100 items (but you certainly can if you want!).
@ -269,7 +269,7 @@ for `a.len() >= 100`?"""
[[exercises]] [[exercises]]
name = "primitive_types4" name = "primitive_types4"
path = "exercises/04_primitive_types/primitive_types4.rs" dir = "04_primitive_types"
mode = "test" mode = "test"
hint = """ hint = """
Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section
@ -284,8 +284,8 @@ https://doc.rust-lang.org/nomicon/coercions.html"""
[[exercises]] [[exercises]]
name = "primitive_types5" name = "primitive_types5"
path = "exercises/04_primitive_types/primitive_types5.rs" dir = "04_primitive_types"
mode = "compile" mode = "run"
hint = """ hint = """
Take a look at the 'Data Types -> The Tuple Type' section of the book: Take a look at the 'Data Types -> The Tuple Type' section of the book:
https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type
@ -297,7 +297,7 @@ of the tuple. You can do it!!"""
[[exercises]] [[exercises]]
name = "primitive_types6" name = "primitive_types6"
path = "exercises/04_primitive_types/primitive_types6.rs" dir = "04_primitive_types"
mode = "test" mode = "test"
hint = """ hint = """
While you could use a destructuring `let` for the tuple here, try While you could use a destructuring `let` for the tuple here, try
@ -310,7 +310,7 @@ Now you have another tool in your toolbox!"""
[[exercises]] [[exercises]]
name = "vecs1" name = "vecs1"
path = "exercises/05_vecs/vecs1.rs" dir = "05_vecs"
mode = "test" mode = "test"
hint = """ hint = """
In Rust, there are two ways to define a Vector. In Rust, there are two ways to define a Vector.
@ -325,7 +325,7 @@ of the Rust book to learn more.
[[exercises]] [[exercises]]
name = "vecs2" name = "vecs2"
path = "exercises/05_vecs/vecs2.rs" dir = "05_vecs"
mode = "test" mode = "test"
hint = """ hint = """
In the first function we are looping over the Vector and getting a reference to In the first function we are looping over the Vector and getting a reference to
@ -348,7 +348,7 @@ What do you think is the more commonly used pattern under Rust developers?
[[exercises]] [[exercises]]
name = "move_semantics1" name = "move_semantics1"
path = "exercises/06_move_semantics/move_semantics1.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
So you've got the "cannot borrow immutable local variable `vec` as mutable" So you've got the "cannot borrow immutable local variable `vec` as mutable"
@ -362,7 +362,7 @@ happens!"""
[[exercises]] [[exercises]]
name = "move_semantics2" name = "move_semantics2"
path = "exercises/06_move_semantics/move_semantics2.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
When running this exercise for the first time, you'll notice an error about When running this exercise for the first time, you'll notice an error about
@ -383,7 +383,7 @@ try them all:
[[exercises]] [[exercises]]
name = "move_semantics3" name = "move_semantics3"
path = "exercises/06_move_semantics/move_semantics3.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
The difference between this one and the previous ones is that the first line The difference between this one and the previous ones is that the first line
@ -393,7 +393,7 @@ an existing binding to be a mutable binding instead of an immutable one :)"""
[[exercises]] [[exercises]]
name = "move_semantics4" name = "move_semantics4"
path = "exercises/06_move_semantics/move_semantics4.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
Stop reading whenever you feel like you have enough direction :) Or try Stop reading whenever you feel like you have enough direction :) Or try
@ -407,7 +407,7 @@ So the end goal is to:
[[exercises]] [[exercises]]
name = "move_semantics5" name = "move_semantics5"
path = "exercises/06_move_semantics/move_semantics5.rs" dir = "06_move_semantics"
mode = "test" mode = "test"
hint = """ hint = """
Carefully reason about the range in which each mutable reference is in Carefully reason about the range in which each mutable reference is in
@ -419,8 +419,8 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref
[[exercises]] [[exercises]]
name = "move_semantics6" name = "move_semantics6"
path = "exercises/06_move_semantics/move_semantics6.rs" dir = "06_move_semantics"
mode = "compile" mode = "run"
hint = """ hint = """
To find the answer, you can consult the book section "References and Borrowing": To find the answer, you can consult the book section "References and Borrowing":
https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html
@ -440,7 +440,7 @@ Another hint: it has to do with the `&` character."""
[[exercises]] [[exercises]]
name = "structs1" name = "structs1"
path = "exercises/07_structs/structs1.rs" dir = "07_structs"
mode = "test" mode = "test"
hint = """ hint = """
Rust has more than one type of struct. Three actually, all variants are used to Rust has more than one type of struct. Three actually, all variants are used to
@ -460,7 +460,7 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html"""
[[exercises]] [[exercises]]
name = "structs2" name = "structs2"
path = "exercises/07_structs/structs2.rs" dir = "07_structs"
mode = "test" mode = "test"
hint = """ hint = """
Creating instances of structs is easy, all you need to do is assign some values Creating instances of structs is easy, all you need to do is assign some values
@ -472,7 +472,7 @@ https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-ins
[[exercises]] [[exercises]]
name = "structs3" name = "structs3"
path = "exercises/07_structs/structs3.rs" dir = "07_structs"
mode = "test" mode = "test"
hint = """ hint = """
For `is_international`: What makes a package international? Seems related to For `is_international`: What makes a package international? Seems related to
@ -488,21 +488,21 @@ https://doc.rust-lang.org/book/ch05-03-method-syntax.html"""
[[exercises]] [[exercises]]
name = "enums1" name = "enums1"
path = "exercises/08_enums/enums1.rs" dir = "08_enums"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "enums2" name = "enums2"
path = "exercises/08_enums/enums2.rs" dir = "08_enums"
mode = "compile" mode = "run"
hint = """ hint = """
You can create enumerations that have different variants with different types You can create enumerations that have different variants with different types
such as no data, anonymous structs, a single string, tuples, ...etc""" such as no data, anonymous structs, a single string, tuples, ...etc"""
[[exercises]] [[exercises]]
name = "enums3" name = "enums3"
path = "exercises/08_enums/enums3.rs" dir = "08_enums"
mode = "test" mode = "test"
hint = """ hint = """
As a first step, you can define enums to compile this code without errors. As a first step, you can define enums to compile this code without errors.
@ -516,8 +516,8 @@ to get value in the variant."""
[[exercises]] [[exercises]]
name = "strings1" name = "strings1"
path = "exercises/09_strings/strings1.rs" dir = "09_strings"
mode = "compile" mode = "run"
hint = """ hint = """
The `current_favorite_color` function is currently returning a string slice The `current_favorite_color` function is currently returning a string slice
with the `'static` lifetime. We know this because the data of the string lives with the `'static` lifetime. We know this because the data of the string lives
@ -530,8 +530,8 @@ another way that uses the `From` trait."""
[[exercises]] [[exercises]]
name = "strings2" name = "strings2"
path = "exercises/09_strings/strings2.rs" dir = "09_strings"
mode = "compile" mode = "run"
hint = """ hint = """
Yes, it would be really easy to fix this by just changing the value bound to Yes, it would be really easy to fix this by just changing the value bound to
`word` to be a string slice instead of a `String`, wouldn't it?? There is a way `word` to be a string slice instead of a `String`, wouldn't it?? There is a way
@ -545,7 +545,7 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion
[[exercises]] [[exercises]]
name = "strings3" name = "strings3"
path = "exercises/09_strings/strings3.rs" dir = "09_strings"
mode = "test" mode = "test"
hint = """ hint = """
There's tons of useful standard library functions for strings. Let's try and use some of them: There's tons of useful standard library functions for strings. Let's try and use some of them:
@ -556,16 +556,16 @@ the string slice into an owned string, which you can then freely extend."""
[[exercises]] [[exercises]]
name = "strings4" name = "strings4"
path = "exercises/09_strings/strings4.rs" dir = "09_strings"
mode = "compile" mode = "run"
hint = "No hints this time ;)" hint = "No hints this time ;)"
# MODULES # MODULES
[[exercises]] [[exercises]]
name = "modules1" name = "modules1"
path = "exercises/10_modules/modules1.rs" dir = "10_modules"
mode = "compile" mode = "run"
hint = """ hint = """
Everything is private in Rust by default-- but there's a keyword we can use Everything is private in Rust by default-- but there's a keyword we can use
to make something public! The compiler error should point to the thing that to make something public! The compiler error should point to the thing that
@ -573,8 +573,8 @@ needs to be public."""
[[exercises]] [[exercises]]
name = "modules2" name = "modules2"
path = "exercises/10_modules/modules2.rs" dir = "10_modules"
mode = "compile" mode = "run"
hint = """ hint = """
The delicious_snacks module is trying to present an external interface that is The delicious_snacks module is trying to present an external interface that is
different than its internal structure (the `fruits` and `veggies` modules and different than its internal structure (the `fruits` and `veggies` modules and
@ -585,8 +585,8 @@ Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-w
[[exercises]] [[exercises]]
name = "modules3" name = "modules3"
path = "exercises/10_modules/modules3.rs" dir = "10_modules"
mode = "compile" mode = "run"
hint = """ hint = """
`UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a
`use` statement for these two to bring them into scope. You can use nested `use` statement for these two to bring them into scope. You can use nested
@ -596,7 +596,7 @@ paths or the glob operator to bring these two in using only one line."""
[[exercises]] [[exercises]]
name = "hashmaps1" name = "hashmaps1"
path = "exercises/11_hashmaps/hashmaps1.rs" dir = "11_hashmaps"
mode = "test" mode = "test"
hint = """ hint = """
Hint 1: Take a look at the return type of the function to figure out Hint 1: Take a look at the return type of the function to figure out
@ -608,7 +608,7 @@ Hint 2: Number of fruits should be at least 5. And you have to put
[[exercises]] [[exercises]]
name = "hashmaps2" name = "hashmaps2"
path = "exercises/11_hashmaps/hashmaps2.rs" dir = "11_hashmaps"
mode = "test" mode = "test"
hint = """ hint = """
Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this.
@ -617,7 +617,7 @@ Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-
[[exercises]] [[exercises]]
name = "hashmaps3" name = "hashmaps3"
path = "exercises/11_hashmaps/hashmaps3.rs" dir = "11_hashmaps"
mode = "test" mode = "test"
hint = """ hint = """
Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert
@ -635,7 +635,6 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v
[[exercises]] [[exercises]]
name = "quiz2" name = "quiz2"
path = "exercises/quiz2.rs"
mode = "test" mode = "test"
hint = "No hints this time ;)" hint = "No hints this time ;)"
@ -643,7 +642,7 @@ hint = "No hints this time ;)"
[[exercises]] [[exercises]]
name = "options1" name = "options1"
path = "exercises/12_options/options1.rs" dir = "12_options"
mode = "test" mode = "test"
hint = """ hint = """
Options can have a `Some` value, with an inner value, or a `None` value, Options can have a `Some` value, with an inner value, or a `None` value,
@ -655,7 +654,7 @@ it doesn't panic in your face later?"""
[[exercises]] [[exercises]]
name = "options2" name = "options2"
path = "exercises/12_options/options2.rs" dir = "12_options"
mode = "test" mode = "test"
hint = """ hint = """
Check out: Check out:
@ -672,8 +671,8 @@ Also see `Option::flatten`
[[exercises]] [[exercises]]
name = "options3" name = "options3"
path = "exercises/12_options/options3.rs" dir = "12_options"
mode = "compile" mode = "run"
hint = """ hint = """
The compiler says a partial move happened in the `match` statement. How can The compiler says a partial move happened in the `match` statement. How can
this be avoided? The compiler shows the correction needed. this be avoided? The compiler shows the correction needed.
@ -685,7 +684,7 @@ https://doc.rust-lang.org/std/keyword.ref.html"""
[[exercises]] [[exercises]]
name = "errors1" name = "errors1"
path = "exercises/13_error_handling/errors1.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
`Ok` and `Err` are the two variants of `Result`, so what the tests are saying `Ok` and `Err` are the two variants of `Result`, so what the tests are saying
@ -701,7 +700,7 @@ To make this change, you'll need to:
[[exercises]] [[exercises]]
name = "errors2" name = "errors2"
path = "exercises/13_error_handling/errors2.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
One way to handle this is using a `match` statement on One way to handle this is using a `match` statement on
@ -717,8 +716,8 @@ and give it a try!"""
[[exercises]] [[exercises]]
name = "errors3" name = "errors3"
path = "exercises/13_error_handling/errors3.rs" dir = "13_error_handling"
mode = "compile" mode = "run"
hint = """ hint = """
If other functions can return a `Result`, why shouldn't `main`? It's a fairly If other functions can return a `Result`, why shouldn't `main`? It's a fairly
common convention to return something like `Result<(), ErrorType>` from your common convention to return something like `Result<(), ErrorType>` from your
@ -729,7 +728,7 @@ positive results."""
[[exercises]] [[exercises]]
name = "errors4" name = "errors4"
path = "exercises/13_error_handling/errors4.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
`PositiveNonzeroInteger::new` is always creating a new instance and returning `PositiveNonzeroInteger::new` is always creating a new instance and returning
@ -741,8 +740,8 @@ everything is... okay :)"""
[[exercises]] [[exercises]]
name = "errors5" name = "errors5"
path = "exercises/13_error_handling/errors5.rs" dir = "13_error_handling"
mode = "compile" mode = "run"
hint = """ hint = """
There are two different possible `Result` types produced within `main()`, which There are two different possible `Result` types produced within `main()`, which
are propagated using `?` operators. How do we declare a return type from are propagated using `?` operators. How do we declare a return type from
@ -765,7 +764,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]] [[exercises]]
name = "errors6" name = "errors6"
path = "exercises/13_error_handling/errors6.rs" dir = "13_error_handling"
mode = "test" mode = "test"
hint = """ hint = """
This exercise uses a completed version of `PositiveNonzeroInteger` from This exercise uses a completed version of `PositiveNonzeroInteger` from
@ -787,8 +786,8 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err"""
[[exercises]] [[exercises]]
name = "generics1" name = "generics1"
path = "exercises/14_generics/generics1.rs" dir = "14_generics"
mode = "compile" mode = "run"
hint = """ hint = """
Vectors in Rust make use of generics to create dynamically sized arrays of any Vectors in Rust make use of generics to create dynamically sized arrays of any
type. type.
@ -797,7 +796,7 @@ You need to tell the compiler what type we are pushing onto this vector."""
[[exercises]] [[exercises]]
name = "generics2" name = "generics2"
path = "exercises/14_generics/generics2.rs" dir = "14_generics"
mode = "test" mode = "test"
hint = """ hint = """
Currently we are wrapping only values of type `u32`. Currently we are wrapping only values of type `u32`.
@ -811,7 +810,7 @@ If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html
[[exercises]] [[exercises]]
name = "traits1" name = "traits1"
path = "exercises/15_traits/traits1.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
A discussion about Traits in Rust can be found at: A discussion about Traits in Rust can be found at:
@ -820,7 +819,7 @@ https://doc.rust-lang.org/book/ch10-02-traits.html
[[exercises]] [[exercises]]
name = "traits2" name = "traits2"
path = "exercises/15_traits/traits2.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
Notice how the trait takes ownership of `self`, and returns `Self`. Notice how the trait takes ownership of `self`, and returns `Self`.
@ -833,7 +832,7 @@ the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html"""
[[exercises]] [[exercises]]
name = "traits3" name = "traits3"
path = "exercises/15_traits/traits3.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
Traits can have a default implementation for functions. Structs that implement Traits can have a default implementation for functions. Structs that implement
@ -845,7 +844,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#def
[[exercises]] [[exercises]]
name = "traits4" name = "traits4"
path = "exercises/15_traits/traits4.rs" dir = "15_traits"
mode = "test" mode = "test"
hint = """ hint = """
Instead of using concrete types as parameters you can use traits. Try replacing Instead of using concrete types as parameters you can use traits. Try replacing
@ -856,8 +855,8 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#tra
[[exercises]] [[exercises]]
name = "traits5" name = "traits5"
path = "exercises/15_traits/traits5.rs" dir = "15_traits"
mode = "compile" mode = "run"
hint = """ hint = """
To ensure a parameter implements multiple traits use the '+ syntax'. Try To ensure a parameter implements multiple traits use the '+ syntax'. Try
replacing the '??' with 'impl <> + <>'. replacing the '??' with 'impl <> + <>'.
@ -869,7 +868,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe
[[exercises]] [[exercises]]
name = "quiz3" name = "quiz3"
path = "exercises/quiz3.rs"
mode = "test" mode = "test"
hint = """ hint = """
To find the best solution to this challenge you're going to need to think back To find the best solution to this challenge you're going to need to think back
@ -881,16 +879,16 @@ You may also need this: `use std::fmt::Display;`."""
[[exercises]] [[exercises]]
name = "lifetimes1" name = "lifetimes1"
path = "exercises/16_lifetimes/lifetimes1.rs" dir = "16_lifetimes"
mode = "compile" mode = "run"
hint = """ hint = """
Let the compiler guide you. Also take a look at the book if you need help: Let the compiler guide you. Also take a look at the book if you need help:
https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html"""
[[exercises]] [[exercises]]
name = "lifetimes2" name = "lifetimes2"
path = "exercises/16_lifetimes/lifetimes2.rs" dir = "16_lifetimes"
mode = "compile" mode = "run"
hint = """ hint = """
Remember that the generic lifetime `'a` will get the concrete lifetime that is Remember that the generic lifetime `'a` will get the concrete lifetime that is
equal to the smaller of the lifetimes of `x` and `y`. equal to the smaller of the lifetimes of `x` and `y`.
@ -903,8 +901,8 @@ inner block:
[[exercises]] [[exercises]]
name = "lifetimes3" name = "lifetimes3"
path = "exercises/16_lifetimes/lifetimes3.rs" dir = "16_lifetimes"
mode = "compile" mode = "run"
hint = """ hint = """
If you use a lifetime annotation in a struct's fields, where else does it need If you use a lifetime annotation in a struct's fields, where else does it need
to be added?""" to be added?"""
@ -913,7 +911,7 @@ to be added?"""
[[exercises]] [[exercises]]
name = "tests1" name = "tests1"
path = "exercises/17_tests/tests1.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
You don't even need to write any code to test -- you can just test values and You don't even need to write any code to test -- you can just test values and
@ -928,7 +926,7 @@ ones pass, and which ones fail :)"""
[[exercises]] [[exercises]]
name = "tests2" name = "tests2"
path = "exercises/17_tests/tests2.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
Like the previous exercise, you don't need to write any code to get this test Like the previous exercise, you don't need to write any code to get this test
@ -941,7 +939,7 @@ argument comes first and which comes second!"""
[[exercises]] [[exercises]]
name = "tests3" name = "tests3"
path = "exercises/17_tests/tests3.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
You can call a function right where you're passing arguments to `assert!`. So You can call a function right where you're passing arguments to `assert!`. So
@ -952,7 +950,7 @@ what you're doing using `!`, like `assert!(!having_fun())`."""
[[exercises]] [[exercises]]
name = "tests4" name = "tests4"
path = "exercises/17_tests/tests4.rs" dir = "17_tests"
mode = "test" mode = "test"
hint = """ hint = """
We expect method `Rectangle::new()` to panic for negative values. We expect method `Rectangle::new()` to panic for negative values.
@ -966,7 +964,7 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa
[[exercises]] [[exercises]]
name = "iterators1" name = "iterators1"
path = "exercises/18_iterators/iterators1.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
Step 1: Step 1:
@ -989,7 +987,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas.
[[exercises]] [[exercises]]
name = "iterators2" name = "iterators2"
path = "exercises/18_iterators/iterators2.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
Step 1: Step 1:
@ -1015,7 +1013,7 @@ powerful and very general. Rust just needs to know the desired type."""
[[exercises]] [[exercises]]
name = "iterators3" name = "iterators3"
path = "exercises/18_iterators/iterators3.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
The `divide` function needs to return the correct error when even division is The `divide` function needs to return the correct error when even division is
@ -1034,7 +1032,7 @@ powerful! It can make the solution to this exercise infinitely easier."""
[[exercises]] [[exercises]]
name = "iterators4" name = "iterators4"
path = "exercises/18_iterators/iterators4.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
In an imperative language, you might write a `for` loop that updates a mutable In an imperative language, you might write a `for` loop that updates a mutable
@ -1046,7 +1044,7 @@ Hint 2: Check out the `fold` and `rfold` methods!"""
[[exercises]] [[exercises]]
name = "iterators5" name = "iterators5"
path = "exercises/18_iterators/iterators5.rs" dir = "18_iterators"
mode = "test" mode = "test"
hint = """ hint = """
The documentation for the `std::iter::Iterator` trait contains numerous methods The documentation for the `std::iter::Iterator` trait contains numerous methods
@ -1065,7 +1063,7 @@ a different method that could make your code more compact than using `fold`."""
[[exercises]] [[exercises]]
name = "box1" name = "box1"
path = "exercises/19_smart_pointers/box1.rs" dir = "19_smart_pointers"
mode = "test" mode = "test"
hint = """ hint = """
Step 1: Step 1:
@ -1089,7 +1087,7 @@ definition and try other types!
[[exercises]] [[exercises]]
name = "rc1" name = "rc1"
path = "exercises/19_smart_pointers/rc1.rs" dir = "19_smart_pointers"
mode = "test" mode = "test"
hint = """ hint = """
This is a straightforward exercise to use the `Rc<T>` type. Each `Planet` has This is a straightforward exercise to use the `Rc<T>` type. Each `Planet` has
@ -1108,8 +1106,8 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html
[[exercises]] [[exercises]]
name = "arc1" name = "arc1"
path = "exercises/19_smart_pointers/arc1.rs" dir = "19_smart_pointers"
mode = "compile" mode = "run"
hint = """ hint = """
Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order
to avoid creating a copy of `numbers`, you'll need to create `child_numbers` to avoid creating a copy of `numbers`, you'll need to create `child_numbers`
@ -1126,7 +1124,7 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html
[[exercises]] [[exercises]]
name = "cow1" name = "cow1"
path = "exercises/19_smart_pointers/cow1.rs" dir = "19_smart_pointers"
mode = "test" mode = "test"
hint = """ hint = """
If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is
@ -1140,8 +1138,8 @@ on the `Cow` type.
[[exercises]] [[exercises]]
name = "threads1" name = "threads1"
path = "exercises/20_threads/threads1.rs" dir = "20_threads"
mode = "compile" mode = "run"
hint = """ hint = """
`JoinHandle` is a struct that is returned from a spawned thread: `JoinHandle` is a struct that is returned from a spawned thread:
https://doc.rust-lang.org/std/thread/fn.spawn.html https://doc.rust-lang.org/std/thread/fn.spawn.html
@ -1158,8 +1156,8 @@ https://doc.rust-lang.org/std/thread/struct.JoinHandle.html
[[exercises]] [[exercises]]
name = "threads2" name = "threads2"
path = "exercises/20_threads/threads2.rs" dir = "20_threads"
mode = "compile" mode = "run"
hint = """ hint = """
`Arc` is an Atomic Reference Counted pointer that allows safe, shared access `Arc` is an Atomic Reference Counted pointer that allows safe, shared access
to **immutable** data. But we want to *change* the number of `jobs_completed` to **immutable** data. But we want to *change* the number of `jobs_completed`
@ -1180,7 +1178,7 @@ https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-betwee
[[exercises]] [[exercises]]
name = "threads3" name = "threads3"
path = "exercises/20_threads/threads3.rs" dir = "20_threads"
mode = "test" mode = "test"
hint = """ hint = """
An alternate way to handle concurrency between threads is to use an `mpsc` An alternate way to handle concurrency between threads is to use an `mpsc`
@ -1199,8 +1197,8 @@ See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info.
[[exercises]] [[exercises]]
name = "macros1" name = "macros1"
path = "exercises/21_macros/macros1.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
When you call a macro, you need to add something special compared to a When you call a macro, you need to add something special compared to a
regular function call. If you're stuck, take a look at what's inside regular function call. If you're stuck, take a look at what's inside
@ -1208,8 +1206,8 @@ regular function call. If you're stuck, take a look at what's inside
[[exercises]] [[exercises]]
name = "macros2" name = "macros2"
path = "exercises/21_macros/macros2.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
Macros don't quite play by the same rules as the rest of Rust, in terms of Macros don't quite play by the same rules as the rest of Rust, in terms of
what's available where. what's available where.
@ -1219,8 +1217,8 @@ Unlike other things in Rust, the order of "where you define a macro" versus
[[exercises]] [[exercises]]
name = "macros3" name = "macros3"
path = "exercises/21_macros/macros3.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
In order to use a macro outside of its module, you need to do something In order to use a macro outside of its module, you need to do something
special to the module to lift the macro out into its parent. special to the module to lift the macro out into its parent.
@ -1230,8 +1228,8 @@ exported macros, if you've seen any of those around."""
[[exercises]] [[exercises]]
name = "macros4" name = "macros4"
path = "exercises/21_macros/macros4.rs" dir = "21_macros"
mode = "compile" mode = "run"
hint = """ hint = """
You only need to add a single character to make this compile. You only need to add a single character to make this compile.
@ -1247,7 +1245,7 @@ https://veykril.github.io/tlborm/"""
[[exercises]] [[exercises]]
name = "clippy1" name = "clippy1"
path = "exercises/22_clippy/clippy1.rs" dir = "22_clippy"
mode = "clippy" mode = "clippy"
hint = """ hint = """
Rust stores the highest precision version of any long or infinite precision Rust stores the highest precision version of any long or infinite precision
@ -1263,14 +1261,14 @@ appropriate replacement constant from `std::f32::consts`..."""
[[exercises]] [[exercises]]
name = "clippy2" name = "clippy2"
path = "exercises/22_clippy/clippy2.rs" dir = "22_clippy"
mode = "clippy" mode = "clippy"
hint = """ hint = """
`for` loops over `Option` values are more clearly expressed as an `if let`""" `for` loops over `Option` values are more clearly expressed as an `if let`"""
[[exercises]] [[exercises]]
name = "clippy3" name = "clippy3"
path = "exercises/22_clippy/clippy3.rs" dir = "22_clippy"
mode = "clippy" mode = "clippy"
hint = "No hints this time!" hint = "No hints this time!"
@ -1278,7 +1276,7 @@ hint = "No hints this time!"
[[exercises]] [[exercises]]
name = "using_as" name = "using_as"
path = "exercises/23_conversions/using_as.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Use the `as` operator to cast one of the operands in the last line of the Use the `as` operator to cast one of the operands in the last line of the
@ -1286,14 +1284,14 @@ Use the `as` operator to cast one of the operands in the last line of the
[[exercises]] [[exercises]]
name = "from_into" name = "from_into"
path = "exercises/23_conversions/from_into.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Follow the steps provided right before the `From` implementation""" Follow the steps provided right before the `From` implementation"""
[[exercises]] [[exercises]]
name = "from_str" name = "from_str"
path = "exercises/23_conversions/from_str.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
The implementation of `FromStr` should return an `Ok` with a `Person` object, The implementation of `FromStr` should return an `Ok` with a `Person` object,
@ -1314,7 +1312,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]] [[exercises]]
name = "try_from_into" name = "try_from_into"
path = "exercises/23_conversions/try_from_into.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Follow the steps provided right before the `TryFrom` implementation. Follow the steps provided right before the `TryFrom` implementation.
@ -1337,7 +1335,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer
[[exercises]] [[exercises]]
name = "as_ref_mut" name = "as_ref_mut"
path = "exercises/23_conversions/as_ref_mut.rs" dir = "23_conversions"
mode = "test" mode = "test"
hint = """ hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions.""" Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""

View file

@ -4,106 +4,128 @@ use crossterm::{
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
ExecutableCommand, ExecutableCommand,
}; };
use serde::{Deserialize, Serialize};
use std::{ use std::{
fs, fs::{self, File},
io::{StdoutLock, Write}, io::{Read, StdoutLock, Write},
}; };
use crate::{exercise::Exercise, FENISH_LINE}; use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE};
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StateFile {
current_exercise_ind: usize,
progress: Vec<bool>,
}
impl StateFile {
fn read(exercises: &[Exercise]) -> Option<Self> {
let file_content = fs::read(".rustlings-state.json").ok()?;
let slf: Self = serde_json::de::from_slice(&file_content).ok()?;
if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() {
return None;
}
Some(slf)
}
fn read_or_default(exercises: &[Exercise]) -> Self {
Self::read(exercises).unwrap_or_else(|| Self {
current_exercise_ind: 0,
progress: vec![false; exercises.len()],
})
}
fn write(&self) -> Result<()> {
let mut buf = Vec::with_capacity(1024);
serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?;
fs::write(".rustlings-state.json", buf)
.context("Failed to write the state file `.rustlings-state.json`")?;
Ok(())
}
}
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
AllDone, AllDone,
Pending, Pending,
} }
pub enum StateFileStatus {
Read,
NotRead,
}
pub struct AppState { pub struct AppState {
state_file: StateFile, current_exercise_ind: usize,
exercises: &'static [Exercise], exercises: Vec<Exercise>,
n_done: u16, n_done: u16,
current_exercise: &'static Exercise, final_message: String,
final_message: &'static str, file_buf: Vec<u8>,
} }
impl AppState { impl AppState {
pub fn new(mut exercises: Vec<Exercise>, mut final_message: String) -> Self { fn update_from_file(&mut self) -> StateFileStatus {
// Leaking especially for sending the exercises to the debounce event handler. self.file_buf.clear();
self.n_done = 0;
if File::open(STATE_FILE_NAME)
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
.is_err()
{
return StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = self.file_buf.split(|c| *c == b'\n');
let Some(current_exercise_name) = lines.next() else {
return StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
return StateFileStatus::NotRead;
}
let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
self.n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
self.current_exercise_ind = ind;
}
}
StateFileStatus::Read
}
pub fn new(
exercise_infos: Vec<ExerciseInfo>,
final_message: String,
) -> (Self, StateFileStatus) {
let exercises = exercise_infos
.into_iter()
.map(|mut exercise_info| {
// Leaking to be able to borrow in the watch mode `Table`.
// Leaking is not a problem because the `AppState` instance lives until // Leaking is not a problem because the `AppState` instance lives until
// the end of the program. // the end of the program.
exercises.shrink_to_fit(); let path = exercise_info.path().leak();
let exercises = exercises.leak();
final_message.shrink_to_fit();
let final_message = final_message.leak();
let state_file = StateFile::read_or_default(exercises); exercise_info.name.shrink_to_fit();
let n_done = state_file let name = exercise_info.name.leak();
.progress
.iter()
.fold(0, |acc, done| acc + u16::from(*done));
let current_exercise = &exercises[state_file.current_exercise_ind];
Self { let hint = exercise_info.hint.trim().to_owned();
state_file,
exercises, Exercise {
n_done, name,
current_exercise, path,
final_message, mode: exercise_info.mode,
hint,
done: false,
} }
})
.collect::<Vec<_>>();
let mut slf = Self {
current_exercise_ind: 0,
exercises,
n_done: 0,
final_message,
file_buf: Vec::with_capacity(2048),
};
let state_file_status = slf.update_from_file();
(slf, state_file_status)
} }
#[inline] #[inline]
pub fn current_exercise_ind(&self) -> usize { pub fn current_exercise_ind(&self) -> usize {
self.state_file.current_exercise_ind self.current_exercise_ind
} }
#[inline] #[inline]
pub fn progress(&self) -> &[bool] { pub fn exercises(&self) -> &[Exercise] {
&self.state_file.progress &self.exercises
}
#[inline]
pub fn exercises(&self) -> &'static [Exercise] {
self.exercises
} }
#[inline] #[inline]
@ -112,8 +134,8 @@ impl AppState {
} }
#[inline] #[inline]
pub fn current_exercise(&self) -> &'static Exercise { pub fn current_exercise(&self) -> &Exercise {
self.current_exercise &self.exercises[self.current_exercise_ind]
} }
pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> {
@ -121,70 +143,61 @@ impl AppState {
bail!(BAD_INDEX_ERR); bail!(BAD_INDEX_ERR);
} }
self.state_file.current_exercise_ind = ind; self.current_exercise_ind = ind;
self.current_exercise = &self.exercises[ind];
self.state_file.write() self.write()
} }
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
let (ind, exercise) = self // O(N) is fine since this method is used only once until the program exits.
// Building a hashmap would have more overhead.
self.current_exercise_ind = self
.exercises .exercises
.iter() .iter()
.enumerate() .position(|exercise| exercise.name == name)
.find(|(_, exercise)| exercise.name == name)
.with_context(|| format!("No exercise found for '{name}'!"))?; .with_context(|| format!("No exercise found for '{name}'!"))?;
self.state_file.current_exercise_ind = ind; self.write()
self.current_exercise = exercise;
self.state_file.write()
} }
pub fn set_pending(&mut self, ind: usize) -> Result<()> { pub fn set_pending(&mut self, ind: usize) -> Result<()> {
let done = self let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?;
.state_file
.progress
.get_mut(ind)
.context(BAD_INDEX_ERR)?;
if *done { if exercise.done {
*done = false; exercise.done = false;
self.n_done -= 1; self.n_done -= 1;
self.state_file.write()?; self.write()?;
} }
Ok(()) Ok(())
} }
fn next_pending_exercise_ind(&self) -> Option<usize> { fn next_pending_exercise_ind(&self) -> Option<usize> {
let current_ind = self.state_file.current_exercise_ind; if self.current_exercise_ind == self.exercises.len() - 1 {
if current_ind == self.state_file.progress.len() - 1 {
// The last exercise is done. // The last exercise is done.
// Search for exercises not done from the start. // Search for exercises not done from the start.
return self.state_file.progress[..current_ind] return self.exercises[..self.current_exercise_ind]
.iter() .iter()
.position(|done| !done); .position(|exercise| !exercise.done);
} }
// The done exercise isn't the last one. // The done exercise isn't the last one.
// Search for a pending exercise after the current one and then from the start. // Search for a pending exercise after the current one and then from the start.
match self.state_file.progress[current_ind + 1..] match self.exercises[self.current_exercise_ind + 1..]
.iter() .iter()
.position(|done| !done) .position(|exercise| !exercise.done)
{ {
Some(ind) => Some(current_ind + 1 + ind), Some(ind) => Some(self.current_exercise_ind + 1 + ind),
None => self.state_file.progress[..current_ind] None => self.exercises[..self.current_exercise_ind]
.iter() .iter()
.position(|done| !done), .position(|exercise| !exercise.done),
} }
} }
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> { pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; let exercise = &mut self.exercises[self.current_exercise_ind];
if !*done { if !exercise.done {
*done = true; exercise.done = true;
self.n_done += 1; self.n_done += 1;
} }
@ -198,15 +211,14 @@ impl AppState {
if !exercise.run()?.status.success() { if !exercise.run()?.status.success() {
writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
self.state_file.current_exercise_ind = exercise_ind; self.current_exercise_ind = exercise_ind;
self.current_exercise = exercise;
// No check if the exercise is done before setting it to pending // No check if the exercise is done before setting it to pending
// because no pending exercise was found. // because no pending exercise was found.
self.state_file.progress[exercise_ind] = false; self.exercises[exercise_ind].done = false;
self.n_done -= 1; self.n_done -= 1;
self.state_file.write()?; self.write()?;
return Ok(ExercisesProgress::Pending); return Ok(ExercisesProgress::Pending);
} }
@ -216,8 +228,12 @@ impl AppState {
writer.execute(Clear(ClearType::All))?; writer.execute(Clear(ClearType::All))?;
writer.write_all(FENISH_LINE.as_bytes())?; writer.write_all(FENISH_LINE.as_bytes())?;
let final_message = self.final_message.trim();
if !final_message.is_empty() {
writer.write_all(self.final_message.as_bytes())?; writer.write_all(self.final_message.as_bytes())?;
writer.write_all(b"\n")?; writer.write_all(b"\n")?;
}
return Ok(ExercisesProgress::AllDone); return Ok(ExercisesProgress::AllDone);
}; };
@ -226,6 +242,32 @@ impl AppState {
Ok(ExercisesProgress::Pending) Ok(ExercisesProgress::Pending)
} }
// Write the state file.
// The file's format is very simple:
// - The first line is the name of the current exercise. It must end with `\n` even if there
// are no done exercises.
// - The second line is an empty line.
// - All remaining lines are the names of done exercises.
fn write(&mut self) -> Result<()> {
self.file_buf.clear();
self.file_buf
.extend_from_slice(self.current_exercise().name.as_bytes());
self.file_buf.push(b'\n');
for exercise in &self.exercises {
if exercise.done {
self.file_buf.push(b'\n');
self.file_buf.extend_from_slice(exercise.name.as_bytes());
}
}
fs::write(STATE_FILE_NAME, &self.file_buf)
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
Ok(())
}
} }
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"

View file

@ -91,7 +91,12 @@ impl EmbeddedFiles {
Ok(()) Ok(())
} }
pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { pub fn write_exercise_to_disk<P>(&self, path: P, strategy: WriteStrategy) -> io::Result<()>
where
P: AsRef<Path>,
{
let path = path.as_ref();
if let Some(file) = self if let Some(file) = self
.exercises_dir .exercises_dir
.files .files

View file

@ -1,77 +1,47 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use crossterm::style::{style, StyledContent, Stylize};
use std::{ use std::{
fmt::{self, Debug, Display, Formatter}, fmt::{self, Display, Formatter},
fs::{self}, fs,
path::PathBuf,
process::{Command, Output}, process::{Command, Output},
}; };
use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::{
embedded::{WriteStrategy, EMBEDDED_FILES},
info_file::Mode,
};
// The mode of the exercise. pub struct TerminalFileLink<'a> {
#[derive(Deserialize, Copy, Clone)] path: &'a str,
#[serde(rename_all = "lowercase")]
pub enum Mode {
// The exercise should be compiled as a binary
Compile,
// The exercise should be compiled as a test harness
Test,
// The exercise should be linted with clippy
Clippy,
} }
#[derive(Deserialize)] impl<'a> Display for TerminalFileLink<'a> {
#[serde(deny_unknown_fields)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
pub struct InfoFile { if let Ok(Some(canonical_path)) = fs::canonicalize(self.path)
// TODO .as_deref()
pub welcome_message: Option<String>, .map(|path| path.to_str())
pub final_message: Option<String>, {
pub exercises: Vec<Exercise>, write!(
} f,
"\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\",
impl InfoFile { canonical_path, self.path,
pub fn parse() -> Result<Self> { )
// Read a local `info.toml` if it exists.
// Mainly to let the tests work for now.
let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") {
toml_edit::de::from_str(&file_content)
} else { } else {
toml_edit::de::from_str(include_str!("../info.toml")) write!(f, "{}", self.path,)
} }
.context("Failed to parse `info.toml`")?;
if slf.exercises.is_empty() {
panic!("{NO_EXERCISES_ERR}");
}
Ok(slf)
} }
} }
// Deserialized from the `info.toml` file.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Exercise { pub struct Exercise {
// Name of the exercise // Exercise's unique name
pub name: String, pub name: &'static str,
// The path to the file containing the exercise's source code // Exercise's path
pub path: PathBuf, pub path: &'static str,
// The mode of the exercise // 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,
} pub done: bool,
// The context information of a pending exercise.
#[derive(PartialEq, Eq, Debug)]
pub struct ContextLine {
// The source code line
pub line: String,
// The line number
pub number: usize,
// Whether this is important and should be highlighted
pub important: bool,
} }
impl Exercise { impl Exercise {
@ -90,7 +60,7 @@ impl Exercise {
.arg("always") .arg("always")
.arg("-q") .arg("-q")
.arg("--bin") .arg("--bin")
.arg(&self.name) .arg(self.name)
.args(args) .args(args)
.output() .output()
.context("Failed to run Cargo") .context("Failed to run Cargo")
@ -98,7 +68,7 @@ impl Exercise {
pub fn run(&self) -> Result<Output> { pub fn run(&self) -> Result<Output> {
match self.mode { match self.mode {
Mode::Compile => self.cargo_cmd("run", &[]), Mode::Run => self.cargo_cmd("run", &[]),
Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]), Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]),
Mode::Clippy => self.cargo_cmd( Mode::Clippy => self.cargo_cmd(
"clippy", "clippy",
@ -109,16 +79,19 @@ impl Exercise {
pub fn reset(&self) -> Result<()> { pub fn reset(&self) -> Result<()> {
EMBEDDED_FILES EMBEDDED_FILES
.write_exercise_to_disk(&self.path, WriteStrategy::Overwrite) .write_exercise_to_disk(self.path, WriteStrategy::Overwrite)
.with_context(|| format!("Failed to reset the exercise {self}")) .with_context(|| format!("Failed to reset the exercise {self}"))
} }
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
style(TerminalFileLink { path: self.path })
.underlined()
.blue()
}
} }
impl Display for Exercise { impl Display for Exercise {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&self.path.display(), f) self.path.fmt(f)
} }
} }
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
If you are developing third-party exercises, add at least one exercise before testing.";

79
src/info_file.rs Normal file
View file

@ -0,0 +1,79 @@
use anyhow::{bail, Context, Error, Result};
use serde::Deserialize;
use std::fs;
// The mode of the exercise.
#[derive(Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
// The exercise should be compiled as a binary
Run,
// The exercise should be compiled as a test harness
Test,
// The exercise should be linted with clippy
Clippy,
}
// Deserialized from the `info.toml` file.
#[derive(Deserialize)]
pub struct ExerciseInfo {
// Name of the exercise
pub name: String,
// The exercise's directory inside the `exercises` directory
pub dir: Option<String>,
// The mode of the exercise
pub mode: Mode,
// The hint text associated with the exercise
pub hint: String,
}
impl ExerciseInfo {
pub fn path(&self) -> String {
if let Some(dir) = &self.dir {
format!("exercises/{dir}/{}.rs", self.name)
} else {
format!("exercises/{}.rs", self.name)
}
}
}
#[derive(Deserialize)]
pub struct InfoFile {
pub welcome_message: Option<String>,
pub final_message: Option<String>,
pub exercises: Vec<ExerciseInfo>,
}
impl InfoFile {
pub fn parse() -> Result<Self> {
// Read a local `info.toml` if it exists.
let slf: Self = match fs::read_to_string("info.toml") {
Ok(file_content) => toml_edit::de::from_str(&file_content)
.context("Failed to parse the `info.toml` file")?,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => {
toml_edit::de::from_str(include_str!("../info.toml"))
.context("Failed to parse the embedded `info.toml` file")?
}
_ => return Err(Error::from(e).context("Failed to read the `info.toml` file")),
},
};
if slf.exercises.is_empty() {
bail!("{NO_EXERCISES_ERR}");
}
let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len());
for exercise in &slf.exercises {
if !names_set.insert(exercise.name.as_str()) {
bail!("Exercise names must all be unique!")
}
}
drop(names_set);
Ok(slf)
}
}
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
If you are developing third-party exercises, add at least one exercise before testing.";

View file

@ -6,17 +6,21 @@ use std::{
path::Path, path::Path,
}; };
use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo};
fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> {
let mut cargo_toml = Vec::with_capacity(1 << 13); let mut cargo_toml = Vec::with_capacity(1 << 13);
cargo_toml.extend_from_slice(b"bin = [\n"); cargo_toml.extend_from_slice(b"bin = [\n");
for exercise in exercises { for exercise_info in exercise_infos {
cargo_toml.extend_from_slice(b" { name = \""); cargo_toml.extend_from_slice(b" { name = \"");
cargo_toml.extend_from_slice(exercise.name.as_bytes()); cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
cargo_toml.extend_from_slice(b"\", path = \""); cargo_toml.extend_from_slice(b"\", path = \"exercises/");
cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); if let Some(dir) = &exercise_info.dir {
cargo_toml.extend_from_slice(b"\" },\n"); cargo_toml.extend_from_slice(dir.as_bytes());
cargo_toml.push(b'/');
}
cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
cargo_toml.extend_from_slice(b".rs\" },\n");
} }
cargo_toml.extend_from_slice( cargo_toml.extend_from_slice(
@ -54,7 +58,7 @@ fn create_vscode_dir() -> Result<()> {
Ok(()) Ok(())
} }
pub fn init(exercises: &[Exercise]) -> Result<()> { pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
} }
@ -74,7 +78,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
.init_exercises_dir() .init_exercises_dir()
.context("Failed to initialize the `rustlings/exercises` directory")?; .context("Failed to initialize the `rustlings/exercises` directory")?;
create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; create_cargo_toml(exercise_infos)
.context("Failed to create the file `rustlings/Cargo.toml`")?;
create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?;
@ -84,7 +89,7 @@ pub fn init(exercises: &[Exercise]) -> Result<()> {
} }
const GITIGNORE: &[u8] = b"/target const GITIGNORE: &[u8] = b"/target
/.rustlings-state.json /.rustlings-state.txt
"; ";
const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;

View file

@ -5,7 +5,7 @@ use crossterm::{
ExecutableCommand, ExecutableCommand,
}; };
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use std::{fmt::Write, io}; use std::io;
mod state; mod state;
@ -72,14 +72,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
ui_state.message.push_str(message); ui_state.message.push_str(message);
} }
KeyCode::Char('r') => { KeyCode::Char('r') => {
let Some(exercise) = ui_state.reset_selected()? else { ui_state = ui_state.with_reset_selected()?;
continue;
};
ui_state = ui_state.with_updated_rows();
ui_state
.message
.write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
} }
KeyCode::Char('c') => { KeyCode::Char('c') => {
ui_state.selected_to_current_exercise()?; ui_state.selected_to_current_exercise()?;

View file

@ -6,8 +6,9 @@ use ratatui::{
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
Frame, Frame,
}; };
use std::fmt::Write;
use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui}; use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter { pub enum Filter {
@ -34,10 +35,9 @@ impl<'a> UiState<'a> {
.app_state .app_state
.exercises() .exercises()
.iter() .iter()
.zip(self.app_state.progress().iter().copied())
.enumerate() .enumerate()
.filter_map(|(ind, (exercise, done))| { .filter_map(|(ind, exercise)| {
let exercise_state = if done { let exercise_state = if exercise.done {
if self.filter == Filter::Pending { if self.filter == Filter::Pending {
return None; return None;
} }
@ -62,8 +62,8 @@ impl<'a> UiState<'a> {
Some(Row::new([ Some(Row::new([
next, next,
exercise_state, exercise_state,
Span::raw(&exercise.name), Span::raw(exercise.name),
Span::raw(exercise.path.to_string_lossy()), Span::raw(exercise.path),
])) ]))
}); });
@ -212,29 +212,30 @@ impl<'a> UiState<'a> {
Ok(()) Ok(())
} }
pub fn reset_selected(&mut self) -> Result<Option<&'static Exercise>> { pub fn with_reset_selected(mut self) -> Result<Self> {
let Some(selected) = self.table_state.selected() else { let Some(selected) = self.table_state.selected() else {
return Ok(None); return Ok(self);
}; };
let (ind, exercise) = self let (ind, exercise) = self
.app_state .app_state
.exercises() .exercises()
.iter() .iter()
.zip(self.app_state.progress())
.enumerate() .enumerate()
.filter_map(|(ind, (exercise, done))| match self.filter { .filter_map(|(ind, exercise)| match self.filter {
Filter::Done => done.then_some((ind, exercise)), Filter::Done => exercise.done.then_some((ind, exercise)),
Filter::Pending => (!done).then_some((ind, exercise)), Filter::Pending => (!exercise.done).then_some((ind, exercise)),
Filter::None => Some((ind, exercise)), Filter::None => Some((ind, exercise)),
}) })
.nth(selected) .nth(selected)
.context("Invalid selection index")?; .context("Invalid selection index")?;
self.app_state.set_pending(ind)?;
exercise.reset()?; exercise.reset()?;
self.message
.write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
self.app_state.set_pending(ind)?;
Ok(Some(exercise)) Ok(self.with_updated_rows())
} }
pub fn selected_to_current_exercise(&mut self) -> Result<()> { pub fn selected_to_current_exercise(&mut self) -> Result<()> {
@ -244,12 +245,12 @@ impl<'a> UiState<'a> {
let ind = self let ind = self
.app_state .app_state
.progress() .exercises()
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(ind, done)| match self.filter { .filter_map(|(ind, exercise)| match self.filter {
Filter::Done => done.then_some(ind), Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!done).then_some(ind), Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some(ind), Filter::None => Some(ind),
}) })
.nth(selected) .nth(selected)

View file

@ -1,10 +1,20 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use app_state::StateFileStatus;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::{path::Path, process::exit}; use crossterm::{
terminal::{Clear, ClearType},
ExecutableCommand,
};
use std::{
io::{self, BufRead, Write},
path::Path,
process::exit,
};
mod app_state; mod app_state;
mod embedded; mod embedded;
mod exercise; mod exercise;
mod info_file;
mod init; mod init;
mod list; mod list;
mod progress_bar; mod progress_bar;
@ -13,7 +23,7 @@ mod watch;
use self::{ use self::{
app_state::AppState, app_state::AppState,
exercise::InfoFile, info_file::InfoFile,
init::init, init::init,
list::list, list::list,
run::run, run::run,
@ -26,6 +36,10 @@ use self::{
struct Args { struct Args {
#[command(subcommand)] #[command(subcommand)]
command: Option<Subcommands>, command: Option<Subcommands>,
/// Manually run the current exercise using `r` or `run` in the watch mode.
/// Only use this if Rustlings fails to detect exercise file changes.
#[arg(long)]
manual_run: bool,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -54,12 +68,10 @@ fn main() -> Result<()> {
which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
let mut info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
info_file.exercises.shrink_to_fit();
let exercises = info_file.exercises;
if matches!(args.command, Some(Subcommands::Init)) { if matches!(args.command, Some(Subcommands::Init)) {
init(&exercises).context("Initialization failed")?; init(&info_file.exercises).context("Initialization failed")?;
println!("{POST_INIT_MSG}"); println!("{POST_INIT_MSG}");
return Ok(()); return Ok(());
@ -68,18 +80,56 @@ fn main() -> Result<()> {
exit(1); exit(1);
} }
let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default()); let (mut app_state, state_file_status) = AppState::new(
info_file.exercises,
info_file.final_message.unwrap_or_default(),
);
if let Some(welcome_message) = info_file.welcome_message {
match state_file_status {
StateFileStatus::NotRead => {
let mut stdout = io::stdout().lock();
stdout.execute(Clear(ClearType::All))?;
let welcome_message = welcome_message.trim();
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.execute(Clear(ClearType::All))?;
}
StateFileStatus::Read => (),
}
}
match args.command { match args.command {
None => loop { None => {
match watch(&mut app_state)? { let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run {
None
} else {
// For the the notify event handler thread.
// Leaking is not a problem because the slice lives until the end of the program.
Some(
app_state
.exercises()
.iter()
.map(|exercise| exercise.path)
.collect::<Vec<_>>()
.leak(),
)
};
loop {
match watch(&mut app_state, notify_exercise_paths)? {
WatchExit::Shutdown => break, WatchExit::Shutdown => break,
// It is much easier to exit the watch mode, launch the list mode and then restart // It is much easier to exit the watch mode, launch the list mode and then restart
// the watch mode instead of trying to pause the watch threads and correct the // the watch mode instead of trying to pause the watch threads and correct the
// watch state. // watch state.
WatchExit::List => list(&mut app_state)?, WatchExit::List => list(&mut app_state)?,
} }
}, }
}
// `Init` is handled above. // `Init` is handled above.
Some(Subcommands::Init) => (), Some(Subcommands::Init) => (),
Some(Subcommands::Run { name }) => { Some(Subcommands::Run { name }) => {
@ -90,10 +140,10 @@ fn main() -> Result<()> {
} }
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
app_state.set_pending(app_state.current_exercise_ind())?;
let exercise = app_state.current_exercise(); let exercise = app_state.current_exercise();
exercise.reset()?; exercise.reset()?;
println!("The exercise {exercise} has been reset!"); println!("The exercise {exercise} has been reset!");
app_state.set_pending(app_state.current_exercise_ind())?;
} }
Some(Subcommands::Hint { name }) => { Some(Subcommands::Hint { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
@ -124,7 +174,7 @@ const POST_INIT_MSG: &str = "
Done initialization! Done initialization!
Run `cd rustlings` to go into the generated directory. Run `cd rustlings` to go into the generated directory.
Then run `rustlings` for further instructions on getting started."; Then run `rustlings` to get started.";
const FENISH_LINE: &str = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |

View file

@ -17,18 +17,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
if !output.status.success() { if !output.status.success() {
app_state.set_pending(app_state.current_exercise_ind())?; app_state.set_pending(app_state.current_exercise_ind())?;
bail!("Ran {exercise} with errors"); bail!(
"Ran {} with errors",
app_state.current_exercise().terminal_link(),
);
} }
stdout.write_fmt(format_args!( stdout.write_fmt(format_args!(
"{}{}\n", "{}{}\n",
"✓ Successfully ran ".green(), "✓ Successfully ran ".green(),
exercise.path.to_string_lossy().green(), exercise.path.green(),
))?; ))?;
match app_state.done_current_exercise(&mut stdout)? { match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::AllDone => (), ExercisesProgress::AllDone => (),
ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), ExercisesProgress::Pending => println!(
"Next exercise: {}",
app_state.current_exercise().terminal_link(),
),
} }
Ok(()) Ok(())

View file

@ -11,14 +11,14 @@ use std::{
time::Duration, time::Duration,
}; };
mod debounce_event; mod notify_event;
mod state; mod state;
mod terminal_event; mod terminal_event;
use crate::app_state::{AppState, ExercisesProgress}; use crate::app_state::{AppState, ExercisesProgress};
use self::{ use self::{
debounce_event::DebounceEventHandler, notify_event::DebounceEventHandler,
state::WatchState, state::WatchState,
terminal_event::{terminal_event_handler, InputEvent}, terminal_event::{terminal_event_handler, InputEvent},
}; };
@ -40,24 +40,40 @@ pub enum WatchExit {
List, List,
} }
pub fn watch(app_state: &mut AppState) -> Result<WatchExit> { pub fn watch(
app_state: &mut AppState,
notify_exercise_paths: Option<&'static [&'static str]>,
) -> Result<WatchExit> {
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut manual_run = false;
// Prevent dropping the guard until the end of the function.
// Otherwise, the file watcher exits.
let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths {
let mut debouncer = new_debouncer( let mut debouncer = new_debouncer(
Duration::from_secs(1), Duration::from_secs(1),
DebounceEventHandler { DebounceEventHandler {
tx: tx.clone(), tx: tx.clone(),
exercises: app_state.exercises(), exercise_paths,
}, },
)?; )
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
debouncer debouncer
.watcher() .watcher()
.watch(Path::new("exercises"), RecursiveMode::Recursive)?; .watch(Path::new("exercises"), RecursiveMode::Recursive)
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
let mut watch_state = WatchState::new(app_state); Some(debouncer)
} else {
manual_run = true;
None
};
let mut watch_state = WatchState::new(app_state, manual_run);
watch_state.run_current_exercise()?; watch_state.run_current_exercise()?;
thread::spawn(move || terminal_event_handler(tx)); thread::spawn(move || terminal_event_handler(tx, manual_run));
while let Ok(event) = rx.recv() { while let Ok(event) = rx.recv() {
match event { match event {
@ -75,6 +91,7 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
watch_state.into_writer().write_all(QUIT_MSG)?; watch_state.into_writer().write_all(QUIT_MSG)?;
break; break;
} }
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
watch_state.handle_invalid_cmd(&cmd)?; watch_state.handle_invalid_cmd(&cmd)?;
} }
@ -85,10 +102,11 @@ pub fn watch(app_state: &mut AppState) -> Result<WatchExit> {
watch_state.render()?; watch_state.render()?;
} }
WatchEvent::NotifyErr(e) => { WatchEvent::NotifyErr(e) => {
return Err(Error::from(e).context("Exercise file watcher failed")) watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?;
return Err(Error::from(e));
} }
WatchEvent::TerminalEventErr(e) => { WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed")) return Err(Error::from(e).context("Terminal event listener failed"));
} }
} }
} }
@ -100,3 +118,11 @@ const QUIT_MSG: &[u8] = b"
We hope you're enjoying learning Rust! We hope you're enjoying learning Rust!
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
"; ";
const NOTIFY_ERR: &str = "
The automatic detection of exercise file changes failed :(
Please try running `rustlings` again.
If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher.
You need to manually trigger running the current exercise using `r` or `run` then.
";

View file

@ -1,13 +1,11 @@
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use crate::exercise::Exercise;
use super::WatchEvent; use super::WatchEvent;
pub struct DebounceEventHandler { pub struct DebounceEventHandler {
pub tx: Sender<WatchEvent>, pub tx: Sender<WatchEvent>,
pub exercises: &'static [Exercise], pub exercise_paths: &'static [&'static str],
} }
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
@ -23,9 +21,9 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
return None; return None;
} }
self.exercises self.exercise_paths
.iter() .iter()
.position(|exercise| event.path.ends_with(&exercise.path)) .position(|path| event.path.ends_with(path))
}) })
.min() .min()
else { else {

View file

@ -18,10 +18,11 @@ pub struct WatchState<'a> {
stderr: Option<Vec<u8>>, stderr: Option<Vec<u8>>,
show_hint: bool, show_hint: bool,
show_done: bool, show_done: bool,
manual_run: bool,
} }
impl<'a> WatchState<'a> { impl<'a> WatchState<'a> {
pub fn new(app_state: &'a mut AppState) -> Self { pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
let writer = io::stdout().lock(); let writer = io::stdout().lock();
Self { Self {
@ -31,6 +32,7 @@ impl<'a> WatchState<'a> {
stderr: None, stderr: None,
show_hint: false, show_hint: false,
show_done: false, show_done: false,
manual_run,
} }
} }
@ -78,6 +80,10 @@ impl<'a> WatchState<'a> {
fn show_prompt(&mut self) -> io::Result<()> { fn show_prompt(&mut self) -> io::Result<()> {
self.writer.write_all(b"\n")?; self.writer.write_all(b"\n")?;
if self.manual_run {
self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?;
}
if self.show_done { if self.show_done {
self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?;
} }
@ -136,11 +142,7 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise
)?; )?;
self.writer.write_fmt(format_args!( self.writer.write_fmt(format_args!(
"{progress_bar}Current exercise: {}\n", "{progress_bar}Current exercise: {}\n",
self.app_state self.app_state.current_exercise().terminal_link(),
.current_exercise()
.path
.to_string_lossy()
.bold(),
))?; ))?;
self.show_prompt()?; self.show_prompt()?;

View file

@ -4,6 +4,7 @@ use std::sync::mpsc::Sender;
use super::WatchEvent; use super::WatchEvent;
pub enum InputEvent { pub enum InputEvent {
Run,
Next, Next,
Hint, Hint,
List, List,
@ -11,7 +12,7 @@ pub enum InputEvent {
Unrecognized(String), Unrecognized(String),
} }
pub fn terminal_event_handler(tx: Sender<WatchEvent>) { pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
let mut input = String::with_capacity(8); let mut input = String::with_capacity(8);
let last_input_event = loop { let last_input_event = loop {
@ -43,6 +44,7 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>) {
"h" | "hint" => InputEvent::Hint, "h" | "hint" => InputEvent::Hint,
"l" | "list" => break InputEvent::List, "l" | "list" => break InputEvent::List,
"q" | "quit" => break InputEvent::Quit, "q" | "quit" => break InputEvent::Quit,
"r" | "run" if manual_run => InputEvent::Run,
_ => InputEvent::Unrecognized(input.clone()), _ => InputEvent::Unrecognized(input.clone()),
}; };

View file

@ -5,34 +5,39 @@ use serde::Deserialize;
use std::fs; use std::fs;
#[derive(Deserialize)] #[derive(Deserialize)]
struct Exercise { struct ExerciseInfo {
name: String, name: String,
path: String, dir: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct InfoToml { struct InfoFile {
exercises: Vec<Exercise>, exercises: Vec<ExerciseInfo>,
} }
#[test] #[test]
fn dev_cargo_bins() { fn dev_cargo_bins() {
let content = fs::read_to_string("dev/Cargo.toml").unwrap(); let cargo_toml = fs::read_to_string("dev/Cargo.toml").unwrap();
let exercises = toml_edit::de::from_str::<InfoToml>(&fs::read_to_string("info.toml").unwrap()) let exercise_infos =
toml_edit::de::from_str::<InfoFile>(&fs::read_to_string("info.toml").unwrap())
.unwrap() .unwrap()
.exercises; .exercises;
let mut start_ind = 0; let mut start_ind = 0;
for exercise in exercises { for exercise_info in exercise_infos {
let name_start = start_ind + content[start_ind..].find('"').unwrap() + 1; let name_start = start_ind + cargo_toml[start_ind..].find('"').unwrap() + 1;
let name_end = name_start + content[name_start..].find('"').unwrap(); let name_end = name_start + cargo_toml[name_start..].find('"').unwrap();
assert_eq!(exercise.name, &content[name_start..name_end]); assert_eq!(exercise_info.name, &cargo_toml[name_start..name_end]);
// +3 to skip `../` at the begeinning of the path. let path_start = name_end + cargo_toml[name_end + 1..].find('"').unwrap() + 2;
let path_start = name_end + content[name_end + 1..].find('"').unwrap() + 5; let path_end = path_start + cargo_toml[path_start..].find('"').unwrap();
let path_end = path_start + content[path_start..].find('"').unwrap(); let expected_path = if let Some(dir) = exercise_info.dir {
assert_eq!(exercise.path, &content[path_start..path_end]); format!("../exercises/{dir}/{}.rs", exercise_info.name)
} else {
format!("../exercises/{}.rs", exercise_info.name)
};
assert_eq!(expected_path, &cargo_toml[path_start..path_end]);
start_ind = path_end + 1; start_ind = path_end + 1;
} }

View file

@ -1,11 +1,9 @@
[[exercises]] [[exercises]]
name = "compFailure" name = "compFailure"
path = "exercises/compFailure.rs" mode = "run"
mode = "compile"
hint = "" hint = ""
[[exercises]] [[exercises]]
name = "testFailure" name = "testFailure"
path = "exercises/testFailure.rs"
mode = "test" mode = "test"
hint = "Hello!" hint = "Hello!"

View file

@ -1,17 +1,14 @@
[[exercises]] [[exercises]]
name = "pending_exercise" name = "pending_exercise"
path = "exercises/pending_exercise.rs" mode = "run"
mode = "compile"
hint = """""" hint = """"""
[[exercises]] [[exercises]]
name = "pending_test_exercise" name = "pending_test_exercise"
path = "exercises/pending_test_exercise.rs"
mode = "test" mode = "test"
hint = """""" hint = """"""
[[exercises]] [[exercises]]
name = "finished_exercise" name = "finished_exercise"
path = "exercises/finished_exercise.rs" mode = "run"
mode = "compile"
hint = """""" hint = """"""

View file

@ -1,11 +1,9 @@
[[exercises]] [[exercises]]
name = "compSuccess" name = "compSuccess"
path = "exercises/compSuccess.rs" mode = "run"
mode = "compile"
hint = """""" hint = """"""
[[exercises]] [[exercises]]
name = "testSuccess" name = "testSuccess"
path = "exercises/testSuccess.rs"
mode = "test" mode = "test"
hint = """""" hint = """"""