Compare commits

...

42 commits

Author SHA1 Message Date
Kacper Poneta 8cfadf8110
Merge 59e8f70e55 into 8df66f7991 2024-08-08 10:58:35 +02:00
mo8it 8df66f7991 Allow initialization in a workspace 2024-08-08 02:45:18 +02:00
mo8it 39580381fa rust-analyzer problem isn't fixed :( 2024-08-08 01:48:57 +02:00
mo8it 06a0f278e5 Don't recommend the builtin VS-Code terminal because it can't clear scrollback 2024-08-08 01:35:47 +02:00
mo8it fd97470f35 Adapt type name in hint 2024-08-08 00:42:26 +02:00
mo8it 11fc3f1e56 Fix errors not being shown after the welcome message 2024-08-08 00:41:12 +02:00
mo8it 693bb708b2 Add README to the solutions dir 2024-08-08 00:41:12 +02:00
mo8it 97719fe8da Remove state file and solutions dir from .gitignore 2024-08-08 00:41:12 +02:00
mo8it 4933ace50b Add panic = "abort" for exercises 2024-08-08 00:41:12 +02:00
mo8it 81bf0a6430 Remove redundant rustfmt check for solutions 2024-08-08 00:41:12 +02:00
mo8it 24aed1b14e Update CHANGELOG 2024-08-08 00:41:12 +02:00
Mo 09c3ac02f8
Merge pull request #2062 from jimbo5922/jimbo5922-fix-hashmap3-struct-name
update struct name in hashmap3
2024-08-08 00:40:51 +02:00
Mo 45a39585b3
Merge pull request #2066 from matthewjnield/main
chore: Fix snakecase convention in errors6.rs
2024-08-08 00:36:46 +02:00
mo8it 286a455fa9 Avoid using RUSTFLAGS to not trigger rebuilding, especially in rust-analyzer 2024-08-07 23:35:50 +02:00
mo8it bdf4960b6a Fix exercise name shift in exercise check 2024-08-07 23:25:22 +02:00
mo8it 13124aafe3 Update deps 2024-08-05 03:15:43 +02:00
Matt Nield 2128be8b28
chore: Fix snakecase convention in errors6.rs
Exercise errors6.rs prompts the user to add a method named `from_parseint`. This commit changes the method name to the corrected snakecase format, `from_parse_int`.
2024-08-04 02:36:45 -04:00
mo8it 175294fa5d Add rust-version 2024-08-02 16:40:06 +02:00
mo8it 5016c7cf7c Use trim_ascii instead of trim 2024-08-02 16:28:05 +02:00
mo8it 1468206052 Stop on first exercise solved 2024-08-02 15:54:14 +02:00
mo8it d1ff4b5cf0 Remove newline 2024-08-01 19:19:25 +02:00
mo8it 700a065abd Fix rustfmt option 2024-08-01 19:19:14 +02:00
mo8it 3fc462f90f Fix tests 2024-08-01 19:17:40 +02:00
mo8it 65a8f6bb4b Run rustfmt on solutions in dev check 2024-08-01 19:14:09 +02:00
mo8it e0f0944bff Refactor check_solutions 2024-08-01 15:53:32 +02:00
mo8it c7590dd752 Improve the runner 2024-08-01 15:23:54 +02:00
mo8it 33a5680328 Hide cargo build warnings if there is no output 2024-08-01 11:28:26 +02:00
mo8it 455d87cadd Fix capacity 2024-08-01 11:26:30 +02:00
Yudai Kawabuchi e65ae09789 fix format 2024-08-01 09:55:25 +09:00
Yudai Kawabuchi dacdce1ea2 fix: update struct name in hashmap3 2024-08-01 09:47:50 +09:00
mo8it 766f3c50ec Add hint to run dev check again after dev update 2024-08-01 01:07:56 +02:00
mo8it 802b97b2ed Set stdin to null when running the binary of an exercise 2024-08-01 01:07:31 +02:00
mo8it 2ad408f2b8 Update deps 2024-07-31 18:54:24 +02:00
mo8it c8fddd8f62 Add Github profile links for every author 2024-07-31 18:53:25 +02:00
mo8it 74fab994e2 Make the output optional 2024-07-28 20:30:23 +02:00
mo8it 3a99542f73 Run the final check in parallel 2024-07-28 17:39:46 +02:00
mo8it 2ae9f3555b Update deps 2024-07-28 13:30:31 +02:00
mo8it 59e8f70e55 Format code 2024-07-12 18:31:23 +02:00
mo8it 4c8365fe88 Update dev/Cargo.toml 2024-07-12 18:25:01 +02:00
Kacper Poneta 52af0674c1 changed the task to make it more appropriate 2024-07-12 18:14:40 +02:00
Kacper Poneta 938b90e5f2 very small solution update 2024-07-11 22:55:48 +02:00
Kacper Poneta 55cc8584bd added exercise 2024-07-11 22:53:38 +02:00
30 changed files with 687 additions and 461 deletions

View file

@ -24,8 +24,6 @@ jobs:
globs: "exercises/**/*.md"
- name: Run cargo fmt
run: cargo fmt --all --check
- name: Run rustfmt on solutions
run: rustfmt --check --edition 2021 --color always solutions/**/*.rs
test:
runs-on: ${{ matrix.os }}
strategy:

View file

@ -1,3 +1,14 @@
<a name="6.2.0"></a>
## 6.2.0 (2024-08-08)
- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports.
- Remove the state file and the solutions directory from the generated `.gitignore` file.
- Add a `README.md` file to the `solutions/` directory.
- Run the final check of all exercises in parallel.
- Small exercise improvements.
- `dev check`: Check that all solutions are formatted with `rustfmt`.
<a name="6.1.0"></a>
## 6.1.0 (2024-07-10)

70
Cargo.lock generated
View file

@ -116,9 +116,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.11"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
dependencies = [
"clap_builder",
"clap_derive",
@ -126,9 +126,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.11"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
dependencies = [
"anstream",
"anstyle",
@ -138,9 +138,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.11"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
@ -264,9 +264,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.2.6"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [
"equivalent",
"hashbrown",
@ -357,9 +357,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
version = "0.12.3"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
dependencies = [
"hashbrown",
]
@ -419,12 +419,12 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "os_pipe"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -587,20 +587,21 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.120"
version = "1.0.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.6"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
@ -617,9 +618,9 @@ dependencies = [
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
@ -698,18 +699,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.16"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
@ -755,9 +756,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
@ -793,11 +794,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -824,6 +825,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -947,9 +957,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.15"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]

View file

@ -8,18 +8,19 @@ exclude = [
[workspace.package]
version = "6.1.0"
authors = [
"Liv <mokou@fastmail.com>",
"Mo Bitar <mo8it@proton.me>",
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
# Alumni
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>", # https://github.com/carols10cents
]
repository = "https://github.com/rust-lang/rustlings"
license = "MIT"
edition = "2021"
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
rust-version = "1.80"
[workspace.dependencies]
serde = { version = "1.0.204", features = ["derive"] }
toml_edit = { version = "0.22.16", default-features = false, features = ["parse", "serde"] }
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] }
[package]
name = "rustlings"
@ -29,6 +30,7 @@ authors.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
keywords = [
"exercise",
"learning",
@ -45,13 +47,13 @@ include = [
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.11", features = ["derive"] }
clap = { version = "4.5.13", features = ["derive"] }
hashbrown = "0.14.5"
notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.2.0"
os_pipe = "1.2.1"
ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] }
rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" }
serde_json = "1.0.120"
serde_json = "1.0.122"
serde.workspace = true
toml_edit.workspace = true
@ -63,3 +65,7 @@ panic = "abort"
[package.metadata.release]
pre-release-hook = ["./release-hook.sh"]
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
[lints.clippy]
needless_option_as_deref = "allow"

View file

@ -17,7 +17,7 @@ It contains code examples and exercises similar to Rustlings, but online.
### Installing Rust
Before installing Rustlings, you need to have _Rust installed_.
Before installing Rustlings, you need to have the **latest version of Rust** installed.
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust.
This will also install _Cargo_, Rust's package/project manager.
@ -88,8 +88,6 @@ While working with Rustlings, please use a modern terminal for the best user exp
The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
If you use VS Code, the builtin terminal should also be fine.
## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.

View file

@ -116,6 +116,8 @@ bin = [
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" },
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
{ name = "generics3", path = "../exercises/14_generics/generics3.rs" },
{ name = "generics3_sol", path = "../solutions/14_generics/generics3.rs" },
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" },
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" },
@ -195,3 +197,9 @@ name = "exercises"
edition = "2021"
# Don't publish the exercises on crates.io!
publish = false
[profile.release]
panic = "abort"
[profile.dev]
panic = "abort"

View file

@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team.
#[derive(Default)]
struct Team {
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new();

View file

@ -25,7 +25,7 @@ impl ParsePosNonzeroError {
}
// TODO: Add another error conversion function here.
// fn from_parseint(???) -> Self { ??? }
// fn from_parse_int(???) -> Self { ??? }
}
#[derive(PartialEq, Debug)]

View file

@ -0,0 +1,54 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// This function should take an array of `Option` elements and returns array of not None elements
// TODO fix this function signature
fn into_dispose_nulls(list: Vec<Option<&str>>) -> Vec<&str> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
#[allow(dead_code)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -9,6 +9,5 @@ cargo upgrades
# Similar to CI
cargo clippy -- --deny warnings
cargo fmt --all --check
rustfmt --check --edition 2021 solutions/**/*.rs
cargo test --workspace --all-targets
cargo run -- dev check --require-solutions

View file

@ -6,6 +6,7 @@ authors.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
include = [
"/src/",
"/info.toml",

View file

@ -571,7 +571,7 @@ name = "hashmaps3"
dir = "11_hashmaps"
hint = """
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
`HashMap` to insert the default value of `Team` if a team doesn't
`HashMap` to insert the default value of `TeamScores` if a team doesn't
exist in the table yet.
Learn more in The Book:
@ -744,6 +744,17 @@ hint = """
Related section in The Book:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions"""
[[exercises]]
name = "generics3"
dir = "14_generics"
hint = """
Vectors in Rust use generics to create dynamically-sized arrays of any type.
The `into_dispose_nulls` function takes a vector as an argument, but only accepts vectors that store the &str type.
To allow the function to accept vectors that store any type, you can leverage your knowledge about generics.
If you're unsure how to proceed, please refer to the Rust Book at:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-function-definitions.
"""
# TRAITS
[[exercises]]

View file

@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team.
#[derive(Default)]
struct Team {
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new();
@ -28,13 +28,17 @@ fn build_scores_table(results: &str) -> HashMap<&str, Team> {
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores.entry(team_1_name).or_insert_with(Team::default);
let team_1 = scores
.entry(team_1_name)
.or_insert_with(TeamScores::default);
// Update the values.
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;
// Similarely for the second team.
let team_2 = scores.entry(team_2_name).or_insert_with(Team::default);
let team_2 = scores
.entry(team_2_name)
.or_insert_with(TeamScores::default);
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;
}

View file

@ -24,7 +24,7 @@ impl ParsePosNonzeroError {
Self::Creation(err)
}
fn from_parseint(err: ParseIntError) -> Self {
fn from_parse_int(err: ParseIntError) -> Self {
Self::ParseInt(err)
}
}
@ -44,7 +44,7 @@ impl PositiveNonzeroInteger {
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
// Return an appropriate error instead of panicking when `parse()`
// returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Self::new(x).map_err(ParsePosNonzeroError::from_creation)
}

View file

@ -0,0 +1,53 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// Here we added generic type `T` to function signature
// Now this function can be used with vector of any
fn into_dispose_nulls<T>(list: Vec<Option<T>>) -> Vec<T> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

6
solutions/README.md Normal file
View file

@ -0,0 +1,6 @@
# Official Rustlings solutions
Before you finish an exercise, its solution file will only contain an empty `main` function.
The content of this file will be automatically replaced by the actual solution once you finish the exercise.
Note that these solution are often only _one possibility_ to solve an exercise.

View file

@ -1,19 +1,18 @@
use anyhow::{bail, Context, Result};
use ratatui::crossterm::style::Stylize;
use serde::Deserialize;
use anyhow::{bail, Context, Error, Result};
use std::{
fs::{self, File},
io::{Read, StdoutLock, Write},
path::{Path, PathBuf},
path::Path,
process::{Command, Stdio},
thread,
};
use crate::{
clear_terminal,
cmd::CmdRunner,
embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY},
exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo,
DEBUG_PROFILE,
};
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
@ -34,31 +33,6 @@ pub enum StateFileStatus {
NotRead,
}
// Parses parts of the output of `cargo metadata`.
#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}
pub fn parse_target_dir() -> Result<PathBuf> {
// Get the target directory from Cargo.
let metadata_output = Command::new("cargo")
.arg("metadata")
.arg("-q")
.arg("--format-version")
.arg("1")
.arg("--no-deps")
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.output()
.context(CARGO_METADATA_ERR)?
.stdout;
serde_json::de::from_slice::<CargoMetadata>(&metadata_output)
.context("Failed to read the field `target_directory` from the `cargo metadata` output")
.map(|metadata| metadata.target_directory)
}
pub struct AppState {
current_exercise_ind: usize,
exercises: Vec<Exercise>,
@ -68,8 +42,7 @@ pub struct AppState {
// Preallocated buffer for reading and writing the state file.
file_buf: Vec<u8>,
official_exercises: bool,
// Cargo's target directory.
target_dir: PathBuf,
cmd_runner: CmdRunner,
}
impl AppState {
@ -123,7 +96,7 @@ impl AppState {
exercise_infos: Vec<ExerciseInfo>,
final_message: String,
) -> Result<(Self, StateFileStatus)> {
let target_dir = parse_target_dir()?;
let cmd_runner = CmdRunner::build()?;
let exercises = exercise_infos
.into_iter()
@ -134,8 +107,7 @@ impl AppState {
let path = exercise_info.path().leak();
let name = exercise_info.name.leak();
let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.trim().to_owned();
let hint = exercise_info.hint.leak().trim_ascii();
Exercise {
dir,
@ -157,7 +129,7 @@ impl AppState {
final_message,
file_buf: Vec::with_capacity(2048),
official_exercises: !Path::new("info.toml").exists(),
target_dir,
cmd_runner,
};
let state_file_status = slf.update_from_file();
@ -186,8 +158,8 @@ impl AppState {
}
#[inline]
pub fn target_dir(&self) -> &Path {
&self.target_dir
pub fn cmd_runner(&self) -> &CmdRunner {
&self.cmd_runner
}
// Write the state file.
@ -336,7 +308,7 @@ impl AppState {
/// Official exercises: Dump the solution file form the binary and return its path.
/// Third-party exercises: Check if a solution file exists and return its path in that case.
pub fn current_solution_path(&self) -> Result<Option<String>> {
if DEBUG_PROFILE {
if cfg!(debug_assertions) {
return Ok(None);
}
@ -373,34 +345,49 @@ impl AppState {
if let Some(ind) = self.next_pending_exercise_ind() {
self.set_current_exercise_ind(ind)?;
return Ok(ExercisesProgress::NewPending);
}
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
write!(writer, "Running {exercise} ... ")?;
writer.flush()?;
let n_exercises = self.exercises.len();
let success = exercise.run_exercise(&mut output, &self.target_dir)?;
if !success {
writeln!(writer, "{}\n", "FAILED".red())?;
let pending_exercise_ind = thread::scope(|s| {
let handles = self
.exercises
.iter_mut()
.map(|exercise| {
s.spawn(|| {
let success = exercise.run_exercise(None, &self.cmd_runner)?;
exercise.done = success;
Ok::<_, Error>(success)
})
})
.collect::<Vec<_>>();
self.current_exercise_ind = exercise_ind;
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?;
writer.flush()?;
// No check if the exercise is done before setting it to pending
// because no pending exercise was found.
self.exercises[exercise_ind].done = false;
self.n_done -= 1;
self.write()?;
return Ok(ExercisesProgress::NewPending);
let success = handle.join().unwrap()?;
if !success {
writer.write_all(b"\n\n")?;
return Ok(Some(exercise_ind));
}
}
writeln!(writer, "{}", "ok".green())?;
Ok::<_, Error>(None)
})?;
if let Some(pending_exercise_ind) = pending_exercise_ind {
self.current_exercise_ind = pending_exercise_ind;
self.n_done = self
.exercises
.iter()
.filter(|exercise| exercise.done)
.count() as u16;
self.write()?;
return Ok(ExercisesProgress::NewPending);
}
// Write that the last exercise is done.
@ -409,7 +396,7 @@ impl AppState {
clear_terminal(writer)?;
writer.write_all(FENISH_LINE.as_bytes())?;
let final_message = self.final_message.trim();
let final_message = self.final_message.trim_ascii();
if !final_message.is_empty() {
writer.write_all(final_message.as_bytes())?;
writer.write_all(b"\n")?;
@ -419,14 +406,9 @@ impl AppState {
}
}
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done.
";
const FENISH_LINE: &str = "+----------------------------------------------------+
@ -462,7 +444,7 @@ mod tests {
path: "exercises/0.rs",
test: false,
strict_clippy: false,
hint: String::new(),
hint: "",
done: false,
}
}
@ -476,7 +458,7 @@ mod tests {
final_message: String::new(),
file_buf: Vec::new(),
official_exercises: true,
target_dir: PathBuf::new(),
cmd_runner: CmdRunner::build().unwrap(),
};
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {

View file

@ -1,30 +1,44 @@
use anyhow::{Context, Result};
use std::{io::Read, path::Path, process::Command};
use serde::Deserialize;
use std::{
io::Read,
path::PathBuf,
process::{Command, Stdio},
};
/// Run a command with a description for a possible error and append the merged stdout and stderr.
/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
let (mut reader, writer) = os_pipe::pipe()
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
let spawn = |mut cmd: Command| {
// NOTE: The closure drops `cmd` which prevents a pipe deadlock.
cmd.stdin(Stdio::null())
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))
};
let writer_clone = writer.try_clone().with_context(|| {
format!("Failed to clone the pipe writer for the command `{description}`")
})?;
let mut handle = if let Some(output) = output {
let (mut reader, writer) = os_pipe::pipe().with_context(|| {
format!("Failed to create a pipe to run the command `{description}``")
})?;
let mut handle = cmd
.stdout(writer_clone)
.stderr(writer)
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))?;
let writer_clone = writer.try_clone().with_context(|| {
format!("Failed to clone the pipe writer for the command `{description}`")
})?;
// Prevent pipe deadlock.
drop(cmd);
cmd.stdout(writer_clone).stderr(writer);
let handle = spawn(cmd)?;
reader
.read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
reader
.read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
output.push(b'\n');
output.push(b'\n');
handle
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null());
spawn(cmd)?
};
handle
.wait()
@ -32,50 +46,100 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Res
.map(|status| status.success())
}
pub struct CargoCmd<'a> {
pub subcommand: &'a str,
pub args: &'a [&'a str],
pub bin_name: &'a str,
pub description: &'a str,
/// RUSTFLAGS="-A warnings"
pub hide_warnings: bool,
/// Added as `--target-dir` if `Self::dev` is true.
pub target_dir: &'a Path,
/// The output buffer to append the merged stdout and stderr.
pub output: &'a mut Vec<u8>,
/// true while developing Rustlings.
pub dev: bool,
// Parses parts of the output of `cargo metadata`.
#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}
impl<'a> CargoCmd<'a> {
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
pub fn run(&mut self) -> Result<bool> {
pub struct CmdRunner {
target_dir: PathBuf,
}
impl CmdRunner {
pub fn build() -> Result<Self> {
// Get the target directory from Cargo.
let metadata_output = Command::new("cargo")
.arg("metadata")
.arg("-q")
.arg("--format-version")
.arg("1")
.arg("--no-deps")
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.output()
.context(CARGO_METADATA_ERR)?
.stdout;
let target_dir = serde_json::de::from_slice::<CargoMetadata>(&metadata_output)
.context("Failed to read the field `target_directory` from the `cargo metadata` output")
.map(|metadata| metadata.target_directory)?;
Ok(Self { target_dir })
}
pub fn cargo<'out>(
&self,
subcommand: &str,
bin_name: &str,
output: Option<&'out mut Vec<u8>>,
) -> CargoSubcommand<'out> {
let mut cmd = Command::new("cargo");
cmd.arg(self.subcommand);
cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name);
// A hack to make `cargo run` work when developing Rustlings.
if self.dev {
cmd.arg("--manifest-path")
.arg("dev/Cargo.toml")
.arg("--target-dir")
.arg(self.target_dir);
#[cfg(debug_assertions)]
cmd.arg("--manifest-path")
.arg("dev/Cargo.toml")
.arg("--target-dir")
.arg(&self.target_dir);
if output.is_some() {
cmd.arg("--color").arg("always");
}
cmd.arg("--color")
.arg("always")
.arg("-q")
.arg("--bin")
.arg(self.bin_name)
.args(self.args);
CargoSubcommand { cmd, output }
}
if self.hide_warnings {
cmd.env("RUSTFLAGS", "-A warnings");
}
/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
// 7 = "/debug/".len()
let mut bin_path =
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
bin_path.push(&self.target_dir);
bin_path.push("debug");
bin_path.push(bin_name);
run_cmd(cmd, self.description, self.output)
run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
}
}
pub struct CargoSubcommand<'out> {
cmd: Command,
output: Option<&'out mut Vec<u8>>,
}
impl<'out> CargoSubcommand<'out> {
#[inline]
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = &'arg str>,
{
self.cmd.args(args);
self
}
/// The boolean in the returned `Result` is true if the command's exit status is success.
#[inline]
pub fn run(self, description: &str) -> Result<bool> {
run_cmd(self.cmd, description, self.output)
}
}
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
#[cfg(test)]
mod tests {
use super::*;
@ -86,7 +150,7 @@ mod tests {
cmd.arg("Hello");
let mut output = Vec::with_capacity(8);
run_cmd(cmd, "echo …", &mut output).unwrap();
run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
assert_eq!(output, b"Hello\n\n");
}

View file

@ -2,8 +2,6 @@ use anyhow::{bail, Context, Result};
use clap::Subcommand;
use std::path::PathBuf;
use crate::DEBUG_PROFILE;
mod check;
mod new;
mod update;
@ -32,7 +30,7 @@ impl DevCommands {
pub fn run(self) -> Result<()> {
match self {
Self::New { path, no_git } => {
if DEBUG_PROFILE {
if cfg!(debug_assertions) {
bail!("Disabled in the debug build");
}

View file

@ -1,22 +1,19 @@
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, bail, Context, Error, Result};
use std::{
cmp::Ordering,
fs::{self, read_dir, OpenOptions},
io::{self, Read, Write},
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Mutex,
},
process::{Command, Stdio},
thread,
};
use crate::{
app_state::parse_target_dir,
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
cmd::CmdRunner,
exercise::{RunnableExercise, OUTPUT_CAPACITY},
info_file::{ExerciseInfo, InfoFile},
CURRENT_FORMAT_VERSION, DEBUG_PROFILE,
CURRENT_FORMAT_VERSION,
};
// Find a char that isn't allowed in the exercise's `name` or `dir`.
@ -24,24 +21,27 @@ fn forbidden_char(input: &str) -> Option<char> {
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
}
// Check that the Cargo.toml file is up-to-date.
// Check that the `Cargo.toml` file is up-to-date.
fn check_cargo_toml(
exercise_infos: &[ExerciseInfo],
current_cargo_toml: &str,
cargo_toml_path: &str,
exercise_path_prefix: &[u8],
) -> Result<()> {
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(&current_cargo_toml)?;
let old_bins = &current_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind];
let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY);
append_bins(&mut new_bins, exercise_infos, exercise_path_prefix);
if old_bins != new_bins {
if DEBUG_PROFILE {
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it");
if cfg!(debug_assertions) {
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
}
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it");
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
}
Ok(())
@ -71,7 +71,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
}
}
if exercise_info.hint.trim().is_empty() {
if exercise_info.hint.trim_ascii().is_empty() {
bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise");
}
@ -162,46 +162,45 @@ fn check_unexpected_files(
Ok(())
}
fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
let error_occurred = AtomicBool::new(false);
fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
println!(
"Running all exercises to check that they aren't already solved. This may take a while…\n",
);
thread::scope(|s| {
for exercise_info in &info_file.exercises {
if exercise_info.skip_check_unsolved {
continue;
}
s.spawn(|| {
let error = |e| {
let mut stderr = io::stderr().lock();
stderr.write_all(e).unwrap();
stderr.write_all(b"\nProblem with the exercise ").unwrap();
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
stderr.write_all(SEPARATOR).unwrap();
error_occurred.store(true, atomic::Ordering::Relaxed);
};
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_exercise(&mut output, target_dir) {
Ok(true) => error(b"Already solved!"),
Ok(false) => (),
Err(e) => error(e.to_string().as_bytes()),
let handles = info_file
.exercises
.iter()
.filter_map(|exercise_info| {
if exercise_info.skip_check_unsolved {
return None;
}
});
Some((
exercise_info.name.as_str(),
s.spawn(|| exercise_info.run_exercise(None, cmd_runner)),
))
})
.collect::<Vec<_>>();
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}");
};
match result {
Ok(true) => bail!(
"The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
),
Ok(false) => (),
Err(e) => return Err(e),
}
}
});
if error_occurred.load(atomic::Ordering::Relaxed) {
bail!(CHECK_EXERCISES_UNSOLVED_ERR);
}
Ok(())
Ok(())
})
}
fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
@ -209,88 +208,123 @@ fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
}
let info_file_paths = check_info_file_exercises(info_file)?;
check_unexpected_files("exercises", &info_file_paths)?;
let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths));
check_exercises_unsolved(info_file, target_dir)
check_exercises_unsolved(info_file, cmd_runner)?;
handle.join().unwrap()
}
fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &Path) -> Result<()> {
let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len()));
let error_occurred = AtomicBool::new(false);
enum SolutionCheck {
Success { sol_path: String },
MissingRequired,
MissingOptional,
RunFailure { output: Vec<u8> },
Err(Error),
}
fn check_solutions(
require_solutions: bool,
info_file: &InfoFile,
cmd_runner: &CmdRunner,
) -> Result<()> {
println!("Running all solutions. This may take a while…\n");
thread::scope(|s| {
for exercise_info in &info_file.exercises {
s.spawn(|| {
let error = |e| {
let mut stderr = io::stderr().lock();
stderr.write_all(e).unwrap();
stderr
.write_all(b"\nFailed to run the solution of the exercise ")
.unwrap();
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
stderr.write_all(SEPARATOR).unwrap();
error_occurred.store(true, atomic::Ordering::Relaxed);
};
let handles = info_file
.exercises
.iter()
.map(|exercise_info| {
s.spawn(|| {
let sol_path = exercise_info.sol_path();
if !Path::new(&sol_path).exists() {
if require_solutions {
return SolutionCheck::MissingRequired;
}
let path = exercise_info.sol_path();
if !Path::new(&path).exists() {
if require_solutions {
error(b"Solution missing");
return SolutionCheck::MissingOptional;
}
// No solution to check.
return;
}
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_solution(&mut output, target_dir) {
Ok(true) => {
paths.lock().unwrap().insert(PathBuf::from(path));
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_solution(Some(&mut output), cmd_runner) {
Ok(true) => SolutionCheck::Success { sol_path },
Ok(false) => SolutionCheck::RunFailure { output },
Err(e) => SolutionCheck::Err(e),
}
Ok(false) => error(&output),
Err(e) => error(e.to_string().as_bytes()),
})
})
.collect::<Vec<_>>();
let mut sol_paths = hashbrown::HashSet::with_capacity(info_file.exercises.len());
let mut fmt_cmd = Command::new("rustfmt");
fmt_cmd
.arg("--check")
.arg("--edition")
.arg("2021")
.arg("--color")
.arg("always")
.stdin(Stdio::null());
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else {
bail!(
"Panic while trying to run the solution of the exericse {}",
exercise_info.name,
);
};
match check_result {
SolutionCheck::Success { sol_path } => {
fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path));
}
});
SolutionCheck::MissingRequired => {
bail!(
"The solution of the exercise {} is missing",
exercise_info.name,
);
}
SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => {
io::stderr().lock().write_all(&output)?;
bail!(
"Running the solution of the exercise {} failed with the error above",
exercise_info.name,
);
}
SolutionCheck::Err(e) => return Err(e),
}
}
});
if error_occurred.load(atomic::Ordering::Relaxed) {
bail!("At least one solution failed. See the output above.");
}
let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths));
check_unexpected_files("solutions", &paths.into_inner().unwrap())?;
if !fmt_cmd
.status()
.context("Failed to run `rustfmt` on all solution files")?
.success()
{
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
}
Ok(())
handle.join().unwrap()
})
}
pub fn check(require_solutions: bool) -> Result<()> {
let info_file = InfoFile::parse()?;
// A hack to make `cargo run -- dev check` work when developing Rustlings.
if DEBUG_PROFILE {
check_cargo_toml(
&info_file.exercises,
include_str!("../../dev-Cargo.toml"),
b"../",
)?;
if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev check` work when developing Rustlings.
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
} else {
let current_cargo_toml =
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
check_cargo_toml(&info_file.exercises, &current_cargo_toml, b"")?;
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
}
let target_dir = parse_target_dir()?;
check_exercises(&info_file, &target_dir)?;
check_solutions(require_solutions, &info_file, &target_dir)?;
let cmd_runner = CmdRunner::build()?;
check_exercises(&info_file, &cmd_runner)?;
check_solutions(require_solutions, &info_file, &cmd_runner)?;
println!("\nEverything looks fine!");
println!("Everything looks fine!");
Ok(())
}
const SEPARATOR: &[u8] =
b"\n========================================================================================\n";
const CHECK_EXERCISES_UNSOLVED_ERR: &str = "At least one exercise is already solved or failed to run. See the output above.
If this is an intro exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file.";
const SKIP_CHECK_UNSOLVED_HINT: &str = "If this is an introduction exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file";

View file

@ -76,8 +76,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
pub const GITIGNORE: &[u8] = b".rustlings-state.txt
Cargo.lock
target
.vscode
target/
.vscode/
!.vscode/extensions.json
";

View file

@ -4,18 +4,19 @@ use std::fs;
use crate::{
cargo_toml::updated_cargo_toml,
info_file::{ExerciseInfo, InfoFile},
DEBUG_PROFILE,
};
// Update the `Cargo.toml` file.
fn update_cargo_toml(
exercise_infos: &[ExerciseInfo],
current_cargo_toml: &str,
exercise_path_prefix: &[u8],
cargo_toml_path: &str,
exercise_path_prefix: &[u8],
) -> Result<()> {
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
let updated_cargo_toml =
updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?;
updated_cargo_toml(exercise_infos, &current_cargo_toml, exercise_path_prefix)?;
fs::write(cargo_toml_path, updated_cargo_toml)
.context("Failed to write the `Cargo.toml` file")?;
@ -26,21 +27,14 @@ fn update_cargo_toml(
pub fn update() -> Result<()> {
let info_file = InfoFile::parse()?;
// A hack to make `cargo run -- dev update` work when developing Rustlings.
if DEBUG_PROFILE {
update_cargo_toml(
&info_file.exercises,
include_str!("../../dev-Cargo.toml"),
b"../",
"dev/Cargo.toml",
)
.context("Failed to update the file `dev/Cargo.toml`")?;
if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev update` work when developing Rustlings.
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
.context("Failed to update the file `dev/Cargo.toml`")?;
println!("Updated `dev/Cargo.toml`");
} else {
let current_cargo_toml =
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
update_cargo_toml(&info_file.exercises, &current_cargo_toml, b"", "Cargo.toml")
update_cargo_toml(&info_file.exercises, "Cargo.toml", &[])
.context("Failed to update the file `Cargo.toml`")?;
println!("Updated `Cargo.toml`");

View file

@ -3,44 +3,39 @@ use ratatui::crossterm::style::{style, StyledContent, Stylize};
use std::{
fmt::{self, Display, Formatter},
io::Write,
path::{Path, PathBuf},
process::Command,
};
use crate::{
cmd::{run_cmd, CargoCmd},
in_official_repo,
terminal_link::TerminalFileLink,
DEBUG_PROFILE,
};
use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink};
/// The initial capacity of the output buffer.
pub const OUTPUT_CAPACITY: usize = 1 << 14;
// Run an exercise binary and append its output to the `output` buffer.
// Compilation must be done before calling this method.
fn run_bin(bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
writeln!(output, "{}", "Output".underlined())?;
fn run_bin(
bin_name: &str,
mut output: Option<&mut Vec<u8>>,
cmd_runner: &CmdRunner,
) -> Result<bool> {
if let Some(output) = output.as_deref_mut() {
writeln!(output, "{}", "Output".underlined())?;
}
// 7 = "/debug/".len()
let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len());
bin_path.push(target_dir);
bin_path.push("debug");
bin_path.push(bin_name);
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
if !success {
// This output is important to show the user that something went wrong.
// Otherwise, calling something like `exit(1)` in an exercise without further output
// leaves the user confused about why the exercise isn't done yet.
writeln!(
output,
"{}",
"The exercise didn't run successfully (nonzero exit code)"
.bold()
.red(),
)?;
if let Some(output) = output {
if !success {
// This output is important to show the user that something went wrong.
// Otherwise, calling something like `exit(1)` in an exercise without further output
// leaves the user confused about why the exercise isn't done yet.
writeln!(
output,
"{}",
"The exercise didn't run successfully (nonzero exit code)"
.bold()
.red(),
)?;
}
}
Ok(success)
@ -54,7 +49,7 @@ pub struct Exercise {
pub path: &'static str,
pub test: bool,
pub strict_clippy: bool,
pub hint: String,
pub hint: &'static str,
pub done: bool,
}
@ -77,89 +72,77 @@ pub trait RunnableExercise {
// Compile, check and run the exercise or its solution (depending on `bin_name´).
// The output is written to the `output` buffer after clearing it.
fn run(&self, bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
output.clear();
// Developing the official Rustlings.
let dev = DEBUG_PROFILE && in_official_repo();
let build_success = CargoCmd {
subcommand: "build",
args: &[],
bin_name,
description: "cargo build …",
hide_warnings: false,
target_dir,
output,
dev,
fn run(
&self,
bin_name: &str,
mut output: Option<&mut Vec<u8>>,
cmd_runner: &CmdRunner,
) -> Result<bool> {
if let Some(output) = output.as_deref_mut() {
output.clear();
}
.run()?;
let build_success = cmd_runner
.cargo("build", bin_name, output.as_deref_mut())
.run("cargo build …")?;
if !build_success {
return Ok(false);
}
// Discard the output of `cargo build` because it will be shown again by Clippy.
output.clear();
// Discard the compiler output because it will be shown again by `cargo test` or Clippy.
if let Some(output) = output.as_deref_mut() {
output.clear();
}
if self.test() {
let output_is_some = output.is_some();
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if output_is_some {
test_cmd.args(["--", "--color", "always", "--show-output"]);
}
let test_success = test_cmd.run("cargo test …")?;
if !test_success {
run_bin(bin_name, output, cmd_runner)?;
return Ok(false);
}
// Discard the compiler output because it will be shown again by Clippy.
if let Some(output) = output.as_deref_mut() {
output.clear();
}
}
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
// `--profile test` is required to also check code with `[cfg(test)]`.
let clippy_args: &[&str] = if self.strict_clippy() {
&["--profile", "test", "--", "-D", "warnings"]
if self.strict_clippy() {
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
} else {
&["--profile", "test"]
};
let clippy_success = CargoCmd {
subcommand: "clippy",
args: clippy_args,
bin_name,
description: "cargo clippy …",
hide_warnings: false,
target_dir,
output,
dev,
}
.run()?;
if !clippy_success {
return Ok(false);
clippy_cmd.args(["--profile", "test"]);
}
if !self.test() {
return run_bin(bin_name, output, target_dir);
}
let clippy_success = clippy_cmd.run("cargo clippy …")?;
let run_success = run_bin(bin_name, output, cmd_runner)?;
let test_success = CargoCmd {
subcommand: "test",
args: &["--", "--color", "always", "--show-output"],
bin_name,
description: "cargo test …",
// Hide warnings because they are shown by Clippy.
hide_warnings: true,
target_dir,
output,
dev,
}
.run()?;
let run_success = run_bin(bin_name, output, target_dir)?;
Ok(test_success && run_success)
Ok(clippy_success && run_success)
}
/// Compile, check and run the exercise.
/// The output is written to the `output` buffer after clearing it.
#[inline]
fn run_exercise(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
self.run(self.name(), output, target_dir)
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
self.run(self.name(), output, cmd_runner)
}
/// Compile, check and run the exercise's solution.
/// The output is written to the `output` buffer after clearing it.
fn run_solution(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
fn run_solution(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
let name = self.name();
let mut bin_name = String::with_capacity(name.len());
let mut bin_name = String::with_capacity(name.len() + 4);
bin_name.push_str(name);
bin_name.push_str("_sol");
self.run(&bin_name, output, target_dir)
self.run(&bin_name, output, cmd_runner)
}
}

View file

@ -3,30 +3,40 @@ use ratatui::crossterm::style::Stylize;
use std::{
env::set_current_dir,
fs::{self, create_dir},
io::ErrorKind,
io::{self, Write},
path::Path,
process::{Command, Stdio},
};
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
use crate::{
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile,
term::press_enter_prompt,
};
pub fn init() -> Result<()> {
// Prevent initialization in a directory that contains the file `Cargo.toml`.
// This can mean that Rustlings was already initialized in this directory.
// Otherwise, this can cause problems with Cargo workspaces.
let rustlings_dir = Path::new("rustlings");
if rustlings_dir.exists() {
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
}
let mut stdout = io::stdout().lock();
let mut init_git = true;
if Path::new("Cargo.toml").exists() {
bail!(CARGO_TOML_EXISTS_ERR);
}
let rustlings_path = Path::new("rustlings");
if let Err(e) = create_dir(rustlings_path) {
if e.kind() == ErrorKind::AlreadyExists {
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
if Path::new("exercises").exists() && Path::new("solutions").exists() {
bail!(IN_INITIALIZED_DIR_ERR);
}
return Err(e.into());
stdout.write_all(CARGO_TOML_EXISTS_PROMPT_MSG)?;
press_enter_prompt(&mut stdout)?;
init_git = false;
}
set_current_dir("rustlings")
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
press_enter_prompt(&mut stdout)?;
create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?;
set_current_dir(rustlings_dir)
.context("Failed to change the current directory to `rustlings/`")?;
let info_file = InfoFile::parse()?;
@ -35,6 +45,11 @@ pub fn init() -> Result<()> {
.context("Failed to initialize the `rustlings/exercises` directory")?;
create_dir("solutions").context("Failed to create the `solutions/` directory")?;
fs::write(
"solutions/README.md",
include_bytes!("../solutions/README.md"),
)
.context("Failed to create the file rustlings/solutions/README.md")?;
for dir in EMBEDDED_FILES.exercise_dirs {
let mut dir_path = String::with_capacity(10 + dir.name.len());
dir_path.push_str("solutions/");
@ -70,18 +85,21 @@ pub fn init() -> Result<()> {
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
// Ignore any Git error because Git initialization is not required.
let _ = Command::new("git")
.arg("init")
.stdin(Stdio::null())
.stderr(Stdio::null())
.status();
if init_git {
// Ignore any Git error because Git initialization is not required.
let _ = Command::new("git")
.arg("init")
.stdin(Stdio::null())
.stderr(Stdio::null())
.status();
}
println!(
writeln!(
stdout,
"\n{}\n\n{}",
"Initialization done ✓".green(),
POST_INIT_MSG.bold(),
);
)?;
Ok(())
}
@ -92,16 +110,14 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
}
";
const GITIGNORE: &[u8] = b".rustlings-state.txt
solutions
Cargo.lock
target
.vscode
const GITIGNORE: &[u8] = b"Cargo.lock
target/
.vscode/
";
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory.
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
Otherwise, please run `rustlings init` again in another directory.";
@ -112,5 +128,19 @@ You probably already initialized Rustlings.
Run `cd rustlings`
Then run `rustlings` again";
const CARGO_TOML_EXISTS_PROMPT_MSG: &[u8] = br#"You are about to initialize Rustlings in a directory that already contains a `Cargo.toml` file!
=> It is recommended to abort with CTRL+C and initialize Rustlings in another directory <=
If you know what you are doing and want to initialize Rustlings in a Cargo workspace,
then you need to add its directory to `members` in the `workspace` section of the `Cargo.toml` file:
```toml
[workspace]
members = ["rustlings"]
```
Press ENTER if you are sure that you want to continue after reading the warning above "#;
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started.";

View file

@ -2,10 +2,11 @@ use anyhow::{bail, Context, Result};
use app_state::StateFileStatus;
use clap::{Parser, Subcommand};
use std::{
io::{self, BufRead, IsTerminal, StdoutLock, Write},
io::{self, IsTerminal, Write},
path::Path,
process::exit,
};
use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
@ -20,35 +21,11 @@ mod init;
mod list;
mod progress_bar;
mod run;
mod term;
mod terminal_link;
mod watch;
const CURRENT_FORMAT_VERSION: u8 = 1;
const DEBUG_PROFILE: bool = {
#[allow(unused_assignments, unused_mut)]
let mut debug_profile = false;
#[cfg(debug_assertions)]
{
debug_profile = true;
}
debug_profile
};
// The current directory is the official Rustligns repository.
fn in_official_repo() -> bool {
Path::new("dev/rustlings-repo.txt").exists()
}
fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
fn press_enter_prompt() -> io::Result<()> {
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
Ok(())
}
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)]
@ -89,20 +66,12 @@ enum Subcommands {
fn main() -> Result<()> {
let args = Args::parse();
if !DEBUG_PROFILE && in_official_repo() {
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
bail!("{OLD_METHOD_ERR}");
}
match args.command {
Some(Subcommands::Init) => {
{
let mut stdout = io::stdout().lock();
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
stdout.flush()?;
press_enter_prompt()?;
stdout.write_all(b"\n")?;
}
return init::init().context("Initialization failed");
}
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
@ -132,11 +101,12 @@ fn main() -> Result<()> {
let mut stdout = io::stdout().lock();
clear_terminal(&mut stdout)?;
let welcome_message = welcome_message.trim();
let welcome_message = welcome_message.trim_ascii();
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
stdout.flush()?;
press_enter_prompt()?;
press_enter_prompt(&mut stdout)?;
clear_terminal(&mut stdout)?;
// Flush to be able to show errors occuring before printing a newline to stdout.
stdout.flush()?;
}
StateFileStatus::Read => (),
}

View file

@ -11,7 +11,7 @@ use crate::{
pub fn run(app_state: &mut AppState) -> Result<()> {
let exercise = app_state.current_exercise();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run_exercise(&mut output, app_state.target_dir())?;
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
let mut stdout = io::stdout().lock();
stdout.write_all(&output)?;

12
src/term.rs Normal file
View file

@ -0,0 +1,12 @@
use std::io::{self, BufRead, StdoutLock, Write};
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")?;
Ok(())
}

View file

@ -54,7 +54,7 @@ impl<'a> WatchState<'a> {
let success = self
.app_state
.current_exercise()
.run_exercise(&mut self.output, self.app_state.target_dir())?;
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
if success {
self.done_status =
if let Some(solution_path) = self.app_state.current_solution_path()? {

View file

@ -1,11 +0,0 @@
bin = [
{ name = "compilation_success", path = "exercises/compilation_success.rs" },
{ name = "compilation_failure", path = "exercises/compilation_failure.rs" },
{ name = "test_success", path = "exercises/test_success.rs" },
{ name = "test_failure", path = "exercises/test_failure.rs" },
]
[package]
name = "test_exercises"
edition = "2021"
publish = false

View file

@ -0,0 +1,11 @@
bin = [
{ name = "compilation_success", path = "../exercises/compilation_success.rs" },
{ name = "compilation_failure", path = "../exercises/compilation_failure.rs" },
{ name = "test_success", path = "../exercises/test_success.rs" },
{ name = "test_failure", path = "../exercises/test_failure.rs" },
]
[package]
name = "test_exercises"
edition = "2021"
publish = false