Compare commits

..

21 commits

Author SHA1 Message Date
Enrico b2f62bdad5
Merge e59c65cf45 into 0432e07864 2024-10-14 11:09:52 -07:00
Mo 0432e07864
Merge pull request #2130 from Nahor/typo
Fix typos
2024-10-14 20:06:30 +02:00
Nahor f33ba139b4 Fix typos 2024-10-14 10:17:17 -07:00
mo8it 990a722852 Limit the maximum number of exercises to 999 2024-10-14 15:57:44 +02:00
mo8it a675cb5754 Replace ahash with foldhash 2024-10-14 15:24:42 +02:00
Mo baeeff389c
Merge pull request #2122 from Nahor/check_all
Improvement to "check all exercises"
2024-10-14 01:29:25 +02:00
mo8it 932bc25d88 Remove unneeded line 2024-10-14 01:28:34 +02:00
mo8it bdc6dad8de Update names 2024-10-14 01:28:12 +02:00
mo8it ea73af9ba3 Separate initialization with a struct 2024-10-14 01:06:11 +02:00
mo8it fc5fc0920f Remove outdated comments 2024-10-14 00:48:12 +02:00
mo8it 9705c161b4 Remove the tracking of done and pending 2024-10-14 00:45:41 +02:00
mo8it 8cac21511c Small improvements to showing progress 2024-10-14 00:42:49 +02:00
mo8it 396ee4d618 Show progress with exercise numbers 2024-10-13 23:28:17 +02:00
mo8it 326169a7fa Improve check-all command 2024-10-13 22:02:41 +02:00
mo8it 685e069c58 First PR review changes 2024-10-10 19:43:35 +02:00
Nahor d3f819f86f Add command line command to check all exercises 2024-10-04 14:36:36 -07:00
Nahor aa83fd6bc4 Show a progress bar when running check_all
Replace the "Progress: xxx/yyy" with a progress bar when checking all
the exercises
2024-10-02 15:28:42 -07:00
Nahor e2f7734f37 Limit the amount of parallelism in check_all
Don't create more threads than there are CPU cores.
2024-10-02 14:42:50 -07:00
Nahor 5c17abd1bf Use a channel to update the check_all progress
The previous code was checking the threads in the order they were
created. So the progress update would be blocked on an earlier thread
even if later thread were already done.

Add to that that multiple instances of `cargo build` cannot run in
parallel, they will be serialized instead. So if the exercises needs to
be recompiled, depending on the order those `cargo build` are run,
the first update can be a long time coming.

So instead of relying on the thread terminating, use a channel to get
notified when an exercise check is done, regardless of the order they
finish in.
2024-10-02 14:10:26 -07:00
Nahor c52867eb8b Add command to check all the exercises
This allows for skipping repeating "next" when multiple exercises
are done at once, or when earlier exercises have been updated/changed
(and thus must be redone) while still working of the whole set (i.e.
the final check_all is not yet available to flag those undone exercises)
2024-10-02 13:40:32 -07:00
Nahor 26fd97a209 Update all exercises during the final check
The previous code run the check on all exercises but only updates one
exercise (the first that failed) even if multiple failed. The user won't
be able to see all the failed exercises when viewing the list, and will
have to run check_all after each fixed exercise.

This change will update all the exercises so the user can see all that
failed, fix them all, and only then need run check_all again.
2024-10-02 11:45:55 -07:00
12 changed files with 350 additions and 161 deletions

86
Cargo.lock generated
View file

@ -2,18 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.15"
@ -71,9 +59,9 @@ checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@ -95,9 +83,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.18" version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -105,9 +93,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.18" version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -198,6 +186,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "foldhash"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
[[package]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "4.1.0" version = "4.1.0"
@ -209,9 +203,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]] [[package]]
name = "heck" name = "heck"
@ -227,9 +221,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -377,9 +371,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "os_pipe" name = "os_pipe"
@ -416,9 +410,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -434,9 +428,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.6" version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
@ -458,10 +452,10 @@ dependencies = [
name = "rustlings" name = "rustlings"
version = "6.3.0" version = "6.3.0"
dependencies = [ dependencies = [
"ahash",
"anyhow", "anyhow",
"clap", "clap",
"crossterm", "crossterm",
"foldhash",
"notify", "notify",
"os_pipe", "os_pipe",
"rustix", "rustix",
@ -587,9 +581,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.77" version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -598,9 +592,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.12.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@ -643,12 +637,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@ -852,23 +840,3 @@ checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -46,10 +46,10 @@ include = [
] ]
[dependencies] [dependencies]
ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.89" anyhow = "1.0.89"
clap = { version = "4.5.18", features = ["derive"] } clap = { version = "4.5.20", features = ["derive"] }
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
foldhash = "0.1.3"
notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] }
os_pipe = "1.2.1" os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
@ -61,7 +61,7 @@ toml_edit.workspace = true
rustix = { version = "0.38.37", default-features = false, features = ["std", "stdio", "termios"] } rustix = { version = "0.38.37", default-features = false, features = ["std", "stdio", "termios"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.12.0" tempfile = "3.13.0"
[profile.release] [profile.release]
panic = "abort" panic = "abort"

View file

@ -5,7 +5,7 @@ disallowed-types = [
] ]
disallowed-methods = [ disallowed-methods = [
# We use `ahash` instead of the default hasher. # We use `foldhash` instead of the default hasher.
"std::collections::HashSet::new", "std::collections::HashSet::new",
"std::collections::HashSet::with_capacity", "std::collections::HashSet::with_capacity",
# Inefficient. Use `.queue(…)` instead. # Inefficient. Use `.queue(…)` instead.
@ -13,4 +13,6 @@ disallowed-methods = [
# Use `thread::Builder::spawn` instead and handle the error. # Use `thread::Builder::spawn` instead and handle the error.
"std::thread::spawn", "std::thread::spawn",
"std::thread::Scope::spawn", "std::thread::Scope::spawn",
# Return `ExitCode` instead.
"std::process::exit",
] ]

View file

@ -1,10 +1,15 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Error, Result};
use crossterm::{cursor, terminal, QueueableCommand};
use std::{ use std::{
env, env,
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::{self, Read, Seek, StdoutLock, Write}, io::{Read, Seek, StdoutLock, Write},
path::{Path, MAIN_SEPARATOR_STR}, path::{Path, MAIN_SEPARATOR_STR},
process::{Command, Stdio}, process::{Command, Stdio},
sync::{
atomic::{AtomicUsize, Ordering::Relaxed},
mpsc,
},
thread, thread,
}; };
@ -15,10 +20,11 @@ use crate::{
embedded::EMBEDDED_FILES, embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise}, exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo, info_file::ExerciseInfo,
term, term::{self, CheckProgressVisualizer},
}; };
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const DEFAULT_CHECK_PARALLELISM: usize = 8;
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
@ -35,10 +41,12 @@ pub enum StateFileStatus {
NotRead, NotRead,
} }
enum AllExercisesCheck { #[derive(Clone, Copy)]
Pending(usize), pub enum CheckProgress {
AllDone, None,
CheckedUntil(usize), Checking,
Done,
Pending,
} }
pub struct AppState { pub struct AppState {
@ -140,11 +148,11 @@ impl AppState {
let mut done_exercises = hash_set_with_capacity(exercises.len()); let mut done_exercises = hash_set_with_capacity(exercises.len());
for done_exerise_name in lines { for done_exercise_name in lines {
if done_exerise_name.is_empty() { if done_exercise_name.is_empty() {
break; break;
} }
done_exercises.insert(done_exerise_name); done_exercises.insert(done_exercise_name);
} }
for (ind, exercise) in exercises.iter_mut().enumerate() { for (ind, exercise) in exercises.iter_mut().enumerate() {
@ -194,6 +202,11 @@ impl AppState {
self.n_done self.n_done
} }
#[inline]
pub fn n_pending(&self) -> u16 {
self.exercises.len() as u16 - self.n_done
}
#[inline] #[inline]
pub fn current_exercise(&self) -> &Exercise { pub fn current_exercise(&self) -> &Exercise {
&self.exercises[self.current_exercise_ind] &self.exercises[self.current_exercise_ind]
@ -270,15 +283,31 @@ impl AppState {
self.write() self.write()
} }
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { // Set the status of an exercise without saving. Returns `true` if the
// status actually changed (and thus needs saving later).
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
let exercise = self let exercise = self
.exercises .exercises
.get_mut(exercise_ind) .get_mut(exercise_ind)
.context(BAD_INDEX_ERR)?; .context(BAD_INDEX_ERR)?;
if exercise.done { if exercise.done == done {
exercise.done = false; return Ok(false);
}
exercise.done = done;
if done {
self.n_done += 1;
} else {
self.n_done -= 1; self.n_done -= 1;
}
Ok(true)
}
// Set the status of an exercise to "pending" and save.
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
if self.set_status(exercise_ind, false)? {
self.write()?; self.write()?;
} }
@ -379,63 +408,114 @@ impl AppState {
} }
} }
// Return the exercise index of the first pending exercise found. fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> { let term_width = terminal::size()
stdout.write_all(FINAL_CHECK_MSG)?; .context("Failed to get the terminal size")?
let n_exercises = self.exercises.len(); .0;
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
let status = thread::scope(|s| { let next_exercise_ind = AtomicUsize::new(0);
let handles = self let mut progresses = vec![CheckProgress::None; self.exercises.len()];
.exercises
.iter()
.map(|exercise| {
thread::Builder::new()
.spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner))
})
.collect::<Vec<_>>();
for (exercise_ind, spawn_res) in handles.into_iter().enumerate() { thread::scope(|s| {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
stdout.flush()?; let n_threads = thread::available_parallelism()
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
let Ok(handle) = spawn_res else { for _ in 0..n_threads {
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); let exercise_progress_sender = exercise_progress_sender.clone();
}; let next_exercise_ind = &next_exercise_ind;
let slf = &self;
thread::Builder::new()
.spawn_scoped(s, move || loop {
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
let Some(exercise) = slf.exercises.get(exercise_ind) else {
// No more exercises.
break;
};
let Ok(success) = handle.join().unwrap() else { if exercise_progress_sender
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); .send((exercise_ind, CheckProgress::Checking))
}; .is_err()
{
break;
};
if !success { let success = exercise.run_exercise(None, &slf.cmd_runner);
return Ok(AllExercisesCheck::Pending(exercise_ind)); let progress = match success {
} Ok(true) => CheckProgress::Done,
Ok(false) => CheckProgress::Pending,
Err(_) => CheckProgress::None,
};
if exercise_progress_sender
.send((exercise_ind, progress))
.is_err()
{
break;
}
})
.context("Failed to spawn a thread to check all exercises")?;
} }
Ok::<_, io::Error>(AllExercisesCheck::AllDone) // Drop this sender to detect when the last thread is done.
drop(exercise_progress_sender);
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
progresses[exercise_ind] = progress;
progress_visualizer.update(&progresses)?;
}
Ok::<_, Error>(())
})?; })?;
let mut exercise_ind = match status { let mut first_pending_exercise_ind = None;
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)), for exercise_ind in 0..progresses.len() {
AllExercisesCheck::AllDone => return Ok(None), match progresses[exercise_ind] {
AllExercisesCheck::CheckedUntil(ind) => ind, CheckProgress::Done => {
}; self.set_status(exercise_ind, true)?;
}
CheckProgress::Pending => {
self.set_status(exercise_ind, false)?;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
CheckProgress::None | CheckProgress::Checking => {
// If we got an error while checking all exercises in parallel,
// it could be because we exceeded the limit of open file descriptors.
// Therefore, try running exercises with errors sequentially.
progresses[exercise_ind] = CheckProgress::Checking;
progress_visualizer.update(&progresses)?;
// We got an error while checking all exercises in parallel. let exercise = &self.exercises[exercise_ind];
// This could be because we exceeded the limit of open file descriptors. let success = exercise.run_exercise(None, &self.cmd_runner)?;
// Therefore, try to continue the check sequentially. if success {
for exercise in &self.exercises[exercise_ind..] { progresses[exercise_ind] = CheckProgress::Done;
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; } else {
stdout.flush()?; progresses[exercise_ind] = CheckProgress::Pending;
if first_pending_exercise_ind.is_none() {
let success = exercise.run_exercise(None, &self.cmd_runner)?; first_pending_exercise_ind = Some(exercise_ind);
if !success { }
return Ok(Some(exercise_ind)); }
self.set_status(exercise_ind, success)?;
progress_visualizer.update(&progresses)?;
}
} }
exercise_ind += 1;
} }
Ok(None) self.write()?;
Ok(first_pending_exercise_ind)
}
// Return the exercise index of the first pending exercise found.
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.queue(cursor::Hide)?;
let res = self.check_all_exercises_impl(stdout);
stdout.queue(cursor::Show)?;
res
} }
/// Mark the current exercise as done and move on to the next pending exercise if one exists. /// Mark the current exercise as done and move on to the next pending exercise if one exists.
@ -462,20 +542,18 @@ impl AppState {
stdout.write_all(b"\n")?; stdout.write_all(b"\n")?;
} }
if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? { if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
stdout.write_all(b"\n\n")?; self.set_current_exercise_ind(first_pending_exercise_ind)?;
self.current_exercise_ind = pending_exercise_ind;
self.exercises[pending_exercise_ind].done = false;
// All exercises were marked as done.
self.n_done -= 1;
self.write()?;
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
// Write that the last exercise is done. self.render_final_message(stdout)?;
self.write()?;
Ok(ExercisesProgress::AllDone)
}
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?; clear_terminal(stdout)?;
stdout.write_all(FENISH_LINE.as_bytes())?; stdout.write_all(FENISH_LINE.as_bytes())?;
@ -485,15 +563,12 @@ impl AppState {
stdout.write_all(b"\n")?; stdout.write_all(b"\n")?;
} }
Ok(ExercisesProgress::AllDone) Ok(())
} }
} }
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";
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
const FINAL_CHECK_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 = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |
+-------------------------- ------------------------+ +-------------------------- ------------------------+

View file

@ -1,10 +1,9 @@
use ahash::AHasher; use foldhash::fast::FixedState;
use std::hash::BuildHasherDefault;
/// DOS attacks aren't a concern for Rustlings. Therefore, we use `ahash` with fixed seeds. /// DOS attacks aren't a concern for Rustlings. Therefore, we use `foldhash` with a fixed state.
pub type HashSet<T> = std::collections::HashSet<T, BuildHasherDefault<AHasher>>; pub type HashSet<T> = std::collections::HashSet<T, FixedState>;
#[inline] #[inline]
pub fn hash_set_with_capacity<T>(capacity: usize) -> HashSet<T> { pub fn hash_set_with_capacity<T>(capacity: usize) -> HashSet<T> {
HashSet::with_capacity_and_hasher(capacity, BuildHasherDefault::<AHasher>::default()) HashSet::with_capacity_and_hasher(capacity, FixedState::default())
} }

View file

@ -17,6 +17,7 @@ use crate::{
CURRENT_FORMAT_VERSION, CURRENT_FORMAT_VERSION,
}; };
const MAX_N_EXERCISES: usize = 999;
const MAX_EXERCISE_NAME_LEN: usize = 32; const MAX_EXERCISE_NAME_LEN: usize = 32;
// Find a char that isn't allowed in the exercise's `name` or `dir`. // Find a char that isn't allowed in the exercise's `name` or `dir`.
@ -201,7 +202,7 @@ fn check_exercises_unsolved(
for (exercise_name, handle) in handles { for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else { let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}"); bail!("Panic while trying to run the exercise {exercise_name}");
}; };
match result { match result {
@ -299,7 +300,7 @@ fn check_solutions(
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else { let Ok(check_result) = handle.join() else {
bail!( bail!(
"Panic while trying to run the solution of the exericse {}", "Panic while trying to run the solution of the exercise {}",
exercise_info.name, exercise_info.name,
); );
}; };
@ -347,6 +348,10 @@ fn check_solutions(
pub fn check(require_solutions: bool) -> Result<()> { pub fn check(require_solutions: bool) -> Result<()> {
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
if info_file.exercises.len() > MAX_N_EXERCISES {
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
}
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// A hack to make `cargo run -- dev check` work when developing Rustlings. // A hack to make `cargo run -- dev check` work when developing Rustlings.
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?; check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;

View file

@ -4,7 +4,7 @@ use clap::{Parser, Subcommand};
use std::{ use std::{
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
path::Path, path::Path,
process::exit, process::ExitCode,
}; };
use term::{clear_terminal, press_enter_prompt}; use term::{clear_terminal, press_enter_prompt};
@ -47,6 +47,8 @@ enum Subcommands {
/// The name of the exercise /// The name of the exercise
name: Option<String>, name: Option<String>,
}, },
/// Check all the exercises, marking them as done or pending accordingly.
CheckAll,
/// Reset a single exercise /// Reset a single exercise
Reset { Reset {
/// The name of the exercise /// The name of the exercise
@ -62,22 +64,26 @@ enum Subcommands {
Dev(DevCommands), Dev(DevCommands),
} }
fn main() -> Result<()> { fn main() -> Result<ExitCode> {
let args = Args::parse(); let args = Args::parse();
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() { if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
bail!("{OLD_METHOD_ERR}"); bail!("{OLD_METHOD_ERR}");
} }
match args.command { 'priority_cmd: {
Some(Subcommands::Init) => return init::init().context("Initialization failed"), match args.command {
Some(Subcommands::Dev(dev_command)) => return dev_command.run(), Some(Subcommands::Init) => init::init().context("Initialization failed")?,
_ => (), Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
_ => break 'priority_cmd,
}
return Ok(ExitCode::SUCCESS);
} }
if !Path::new("exercises").is_dir() { if !Path::new("exercises").is_dir() {
println!("{PRE_INIT_MSG}"); println!("{PRE_INIT_MSG}");
exit(1); return Ok(ExitCode::FAILURE);
} }
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
@ -136,7 +142,35 @@ fn main() -> Result<()> {
if let Some(name) = name { if let Some(name) = name {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
} }
run::run(&mut app_state)?; return run::run(&mut app_state);
}
Some(Subcommands::CheckAll) => {
let mut stdout = io::stdout().lock();
if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? {
if app_state.current_exercise().done {
app_state.set_current_exercise_ind(first_pending_exercise_ind)?;
}
stdout.write_all(b"\n\n")?;
let pending = app_state.n_pending();
if pending == 1 {
stdout.write_all(b"One exercise pending: ")?;
} else {
write!(
stdout,
"{pending}/{} exercises pending. The first: ",
app_state.exercises().len(),
)?;
}
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b"\n")?;
return Ok(ExitCode::FAILURE);
} else {
app_state.render_final_message(&mut stdout)?;
}
} }
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
@ -153,7 +187,7 @@ fn main() -> Result<()> {
Some(Subcommands::Init | Subcommands::Dev(_)) => (), Some(Subcommands::Init | Subcommands::Dev(_)) => (),
} }
Ok(()) Ok(ExitCode::SUCCESS)
} }
const OLD_METHOD_ERR: &str = const OLD_METHOD_ERR: &str =

View file

@ -5,7 +5,7 @@ use crossterm::{
}; };
use std::{ use std::{
io::{self, Write}, io::{self, Write},
process::exit, process::ExitCode,
}; };
use crate::{ use crate::{
@ -13,7 +13,7 @@ use crate::{
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
}; };
pub fn run(app_state: &mut AppState) -> Result<()> { pub fn run(app_state: &mut AppState) -> Result<ExitCode> {
let exercise = app_state.current_exercise(); let exercise = app_state.current_exercise();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?; let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
@ -29,7 +29,8 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
.current_exercise() .current_exercise()
.terminal_file_link(&mut stdout)?; .terminal_file_link(&mut stdout)?;
stdout.write_all(b" with errors\n")?; stdout.write_all(b" with errors\n")?;
exit(1);
return Ok(ExitCode::FAILURE);
} }
stdout.queue(SetForegroundColor(Color::Green))?; stdout.queue(SetForegroundColor(Color::Green))?;
@ -55,5 +56,5 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
ExercisesProgress::AllDone => (), ExercisesProgress::AllDone => (),
} }
Ok(()) Ok(ExitCode::SUCCESS)
} }

View file

@ -1,6 +1,6 @@
use crossterm::{ use crossterm::{
cursor::MoveTo, cursor::MoveTo,
style::{Attribute, Color, SetAttribute, SetForegroundColor}, style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
Command, QueueableCommand, Command, QueueableCommand,
}; };
@ -9,6 +9,8 @@ use std::{
io::{self, BufRead, StdoutLock, Write}, io::{self, BufRead, StdoutLock, Write},
}; };
use crate::app_state::CheckProgress;
pub struct MaxLenWriter<'a, 'b> { pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'b>, pub stdout: &'a mut StdoutLock<'b>,
len: usize, len: usize,
@ -85,14 +87,86 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> {
} }
} }
/// Terminal progress bar to be used when not using Ratataui. pub struct CheckProgressVisualizer<'a, 'b> {
stdout: &'a mut StdoutLock<'b>,
n_cols: usize,
}
impl<'a, 'b> CheckProgressVisualizer<'a, 'b> {
const CHECKING_COLOR: Color = Color::Blue;
const DONE_COLOR: Color = Color::Green;
const PENDING_COLOR: Color = Color::Red;
pub fn build(stdout: &'a mut StdoutLock<'b>, term_width: u16) -> io::Result<Self> {
clear_terminal(stdout)?;
stdout.write_all("Checking all exercises…\n".as_bytes())?;
// Legend
stdout.write_all(b"Color of exercise number: ")?;
stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
stdout.write_all(b"Checking")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
stdout.write_all(b"Done")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
stdout.write_all(b"Pending")?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
// Exercise numbers with up to 3 digits.
// +1 because the last column doesn't end with a whitespace.
let n_cols = usize::from(term_width + 1) / 4;
Ok(Self { stdout, n_cols })
}
pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> {
self.stdout.queue(MoveTo(0, 2))?;
let mut exercise_num = 1;
for exercise_progress in progresses {
match exercise_progress {
CheckProgress::None => (),
CheckProgress::Checking => {
self.stdout
.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
}
CheckProgress::Done => {
self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
}
CheckProgress::Pending => {
self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
}
}
write!(self.stdout, "{exercise_num:<3}")?;
self.stdout.queue(ResetColor)?;
if exercise_num != progresses.len() {
if exercise_num % self.n_cols == 0 {
self.stdout.write_all(b"\n")?;
} else {
self.stdout.write_all(b" ")?;
}
exercise_num += 1;
}
}
self.stdout.flush()
}
}
pub fn progress_bar<'a>( pub fn progress_bar<'a>(
writer: &mut impl CountedWrite<'a>, writer: &mut impl CountedWrite<'a>,
progress: u16, progress: u16,
total: u16, total: u16,
line_width: u16, term_width: u16,
) -> io::Result<()> { ) -> io::Result<()> {
debug_assert!(total < 1000); debug_assert!(total <= 999);
debug_assert!(progress <= total); debug_assert!(progress <= total);
const PREFIX: &[u8] = b"Progress: ["; const PREFIX: &[u8] = b"Progress: [";
@ -101,7 +175,7 @@ pub fn progress_bar<'a>(
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
if line_width < MIN_LINE_WIDTH { if term_width < MIN_LINE_WIDTH {
writer.write_ascii(b"Progress: ")?; writer.write_ascii(b"Progress: ")?;
// Integers are in ASCII. // Integers are in ASCII.
return writer.write_ascii(format!("{progress}/{total}").as_bytes()); return writer.write_ascii(format!("{progress}/{total}").as_bytes());
@ -110,7 +184,7 @@ pub fn progress_bar<'a>(
let stdout = writer.stdout(); let stdout = writer.stdout();
stdout.write_all(PREFIX)?; stdout.write_all(PREFIX)?;
let width = line_width - WRAPPER_WIDTH; let width = term_width - WRAPPER_WIDTH;
let filled = (width * progress) / total; let filled = (width * progress) / total;
stdout.queue(SetForegroundColor(Color::Green))?; stdout.queue(SetForegroundColor(Color::Green))?;

View file

@ -103,6 +103,13 @@ fn run_watch(
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
WatchEvent::Input(InputEvent::CheckAll) => match watch_state
.check_all_exercises(&mut stdout)?
{
ExercisesProgress::AllDone => break,
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
},
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Quit) => { WatchEvent::Input(InputEvent::Quit) => {
stdout.write_all(QUIT_MSG)?; stdout.write_all(QUIT_MSG)?;

View file

@ -157,8 +157,9 @@ impl<'a> WatchState<'a> {
/// Move on to the next exercise if the current one is done. /// Move on to the next exercise if the current one is done.
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> { pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
if self.done_status == DoneStatus::Pending { match self.done_status {
return Ok(ExercisesProgress::CurrentPending); DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (),
DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending),
} }
self.app_state.done_current_exercise::<true>(stdout) self.app_state.done_current_exercise::<true>(stdout)
@ -195,6 +196,11 @@ impl<'a> WatchState<'a> {
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
stdout.write_all(b":list / ")?; stdout.write_all(b":list / ")?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"c")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":check all / ")?;
stdout.queue(SetAttribute(Attribute::Bold))?; stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"x")?; stdout.write_all(b"x")?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
@ -274,6 +280,22 @@ impl<'a> WatchState<'a> {
Ok(()) Ok(())
} }
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? {
// Only change exercise if the current one is done.
if self.app_state.current_exercise().done {
self.app_state
.set_current_exercise_ind(first_pending_exercise_ind)?;
Ok(ExercisesProgress::NewPending)
} else {
Ok(ExercisesProgress::CurrentPending)
}
} else {
self.app_state.render_final_message(stdout)?;
Ok(ExercisesProgress::AllDone)
}
}
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> { pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_width != width { if self.term_width != width {
self.term_width = width; self.term_width = width;

View file

@ -11,6 +11,7 @@ pub enum InputEvent {
Run, Run,
Hint, Hint,
List, List,
CheckAll,
Reset, Reset,
Quit, Quit,
} }
@ -37,6 +38,7 @@ pub fn terminal_event_handler(
KeyCode::Char('r') if manual_run => InputEvent::Run, KeyCode::Char('r') if manual_run => InputEvent::Run,
KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('h') => InputEvent::Hint,
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
KeyCode::Char('c') => InputEvent::CheckAll,
KeyCode::Char('x') => { KeyCode::Char('x') => {
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() { if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
return; return;