Skip to content

Commit baeeff3

Browse files
authored
Merge pull request #2122 from Nahor/check_all
Improvement to "check all exercises"
2 parents 84a42a2 + 932bc25 commit baeeff3

File tree

8 files changed

+309
-92
lines changed

8 files changed

+309
-92
lines changed

clippy.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ disallowed-methods = [
1313
# Use `thread::Builder::spawn` instead and handle the error.
1414
"std::thread::spawn",
1515
"std::thread::Scope::spawn",
16+
# Return `ExitCode` instead.
17+
"std::process::exit",
1618
]

src/app_state.rs

Lines changed: 147 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use anyhow::{bail, Context, Result};
1+
use anyhow::{bail, Context, Error, Result};
2+
use crossterm::{cursor, terminal, QueueableCommand};
23
use std::{
34
env,
45
fs::{File, OpenOptions},
5-
io::{self, Read, Seek, StdoutLock, Write},
6+
io::{Read, Seek, StdoutLock, Write},
67
path::{Path, MAIN_SEPARATOR_STR},
78
process::{Command, Stdio},
9+
sync::{
10+
atomic::{AtomicUsize, Ordering::Relaxed},
11+
mpsc,
12+
},
813
thread,
914
};
1015

@@ -15,10 +20,11 @@ use crate::{
1520
embedded::EMBEDDED_FILES,
1621
exercise::{Exercise, RunnableExercise},
1722
info_file::ExerciseInfo,
18-
term,
23+
term::{self, CheckProgressVisualizer},
1924
};
2025

2126
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
27+
const DEFAULT_CHECK_PARALLELISM: usize = 8;
2228

2329
#[must_use]
2430
pub enum ExercisesProgress {
@@ -35,10 +41,12 @@ pub enum StateFileStatus {
3541
NotRead,
3642
}
3743

38-
enum AllExercisesCheck {
39-
Pending(usize),
40-
AllDone,
41-
CheckedUntil(usize),
44+
#[derive(Clone, Copy)]
45+
pub enum CheckProgress {
46+
None,
47+
Checking,
48+
Done,
49+
Pending,
4250
}
4351

4452
pub struct AppState {
@@ -194,6 +202,11 @@ impl AppState {
194202
self.n_done
195203
}
196204

205+
#[inline]
206+
pub fn n_pending(&self) -> u16 {
207+
self.exercises.len() as u16 - self.n_done
208+
}
209+
197210
#[inline]
198211
pub fn current_exercise(&self) -> &Exercise {
199212
&self.exercises[self.current_exercise_ind]
@@ -270,15 +283,31 @@ impl AppState {
270283
self.write()
271284
}
272285

273-
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
286+
// Set the status of an exercise without saving. Returns `true` if the
287+
// status actually changed (and thus needs saving later).
288+
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
274289
let exercise = self
275290
.exercises
276291
.get_mut(exercise_ind)
277292
.context(BAD_INDEX_ERR)?;
278293

279-
if exercise.done {
280-
exercise.done = false;
294+
if exercise.done == done {
295+
return Ok(false);
296+
}
297+
298+
exercise.done = done;
299+
if done {
300+
self.n_done += 1;
301+
} else {
281302
self.n_done -= 1;
303+
}
304+
305+
Ok(true)
306+
}
307+
308+
// Set the status of an exercise to "pending" and save.
309+
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
310+
if self.set_status(exercise_ind, false)? {
282311
self.write()?;
283312
}
284313

@@ -379,63 +408,114 @@ impl AppState {
379408
}
380409
}
381410

382-
// Return the exercise index of the first pending exercise found.
383-
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
384-
stdout.write_all(FINAL_CHECK_MSG)?;
385-
let n_exercises = self.exercises.len();
386-
387-
let status = thread::scope(|s| {
388-
let handles = self
389-
.exercises
390-
.iter()
391-
.map(|exercise| {
392-
thread::Builder::new()
393-
.spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner))
394-
})
395-
.collect::<Vec<_>>();
396-
397-
for (exercise_ind, spawn_res) in handles.into_iter().enumerate() {
398-
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
399-
stdout.flush()?;
400-
401-
let Ok(handle) = spawn_res else {
402-
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
403-
};
404-
405-
let Ok(success) = handle.join().unwrap() else {
406-
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
407-
};
408-
409-
if !success {
410-
return Ok(AllExercisesCheck::Pending(exercise_ind));
411-
}
411+
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
412+
let term_width = terminal::size()
413+
.context("Failed to get the terminal size")?
414+
.0;
415+
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
416+
417+
let next_exercise_ind = AtomicUsize::new(0);
418+
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
419+
420+
thread::scope(|s| {
421+
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
422+
let n_threads = thread::available_parallelism()
423+
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
424+
425+
for _ in 0..n_threads {
426+
let exercise_progress_sender = exercise_progress_sender.clone();
427+
let next_exercise_ind = &next_exercise_ind;
428+
let slf = &self;
429+
thread::Builder::new()
430+
.spawn_scoped(s, move || loop {
431+
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
432+
let Some(exercise) = slf.exercises.get(exercise_ind) else {
433+
// No more exercises.
434+
break;
435+
};
436+
437+
if exercise_progress_sender
438+
.send((exercise_ind, CheckProgress::Checking))
439+
.is_err()
440+
{
441+
break;
442+
};
443+
444+
let success = exercise.run_exercise(None, &slf.cmd_runner);
445+
let progress = match success {
446+
Ok(true) => CheckProgress::Done,
447+
Ok(false) => CheckProgress::Pending,
448+
Err(_) => CheckProgress::None,
449+
};
450+
451+
if exercise_progress_sender
452+
.send((exercise_ind, progress))
453+
.is_err()
454+
{
455+
break;
456+
}
457+
})
458+
.context("Failed to spawn a thread to check all exercises")?;
412459
}
413460

414-
Ok::<_, io::Error>(AllExercisesCheck::AllDone)
415-
})?;
461+
// Drop this sender to detect when the last thread is done.
462+
drop(exercise_progress_sender);
416463

417-
let mut exercise_ind = match status {
418-
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)),
419-
AllExercisesCheck::AllDone => return Ok(None),
420-
AllExercisesCheck::CheckedUntil(ind) => ind,
421-
};
464+
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
465+
progresses[exercise_ind] = progress;
466+
progress_visualizer.update(&progresses)?;
467+
}
422468

423-
// We got an error while checking all exercises in parallel.
424-
// This could be because we exceeded the limit of open file descriptors.
425-
// Therefore, try to continue the check sequentially.
426-
for exercise in &self.exercises[exercise_ind..] {
427-
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
428-
stdout.flush()?;
469+
Ok::<_, Error>(())
470+
})?;
429471

430-
let success = exercise.run_exercise(None, &self.cmd_runner)?;
431-
if !success {
432-
return Ok(Some(exercise_ind));
472+
let mut first_pending_exercise_ind = None;
473+
for exercise_ind in 0..progresses.len() {
474+
match progresses[exercise_ind] {
475+
CheckProgress::Done => {
476+
self.set_status(exercise_ind, true)?;
477+
}
478+
CheckProgress::Pending => {
479+
self.set_status(exercise_ind, false)?;
480+
if first_pending_exercise_ind.is_none() {
481+
first_pending_exercise_ind = Some(exercise_ind);
482+
}
483+
}
484+
CheckProgress::None | CheckProgress::Checking => {
485+
// If we got an error while checking all exercises in parallel,
486+
// it could be because we exceeded the limit of open file descriptors.
487+
// Therefore, try running exercises with errors sequentially.
488+
progresses[exercise_ind] = CheckProgress::Checking;
489+
progress_visualizer.update(&progresses)?;
490+
491+
let exercise = &self.exercises[exercise_ind];
492+
let success = exercise.run_exercise(None, &self.cmd_runner)?;
493+
if success {
494+
progresses[exercise_ind] = CheckProgress::Done;
495+
} else {
496+
progresses[exercise_ind] = CheckProgress::Pending;
497+
if first_pending_exercise_ind.is_none() {
498+
first_pending_exercise_ind = Some(exercise_ind);
499+
}
500+
}
501+
self.set_status(exercise_ind, success)?;
502+
progress_visualizer.update(&progresses)?;
503+
}
433504
}
434-
435-
exercise_ind += 1;
436505
}
437506

438-
Ok(None)
507+
self.write()?;
508+
509+
Ok(first_pending_exercise_ind)
510+
}
511+
512+
// Return the exercise index of the first pending exercise found.
513+
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
514+
stdout.queue(cursor::Hide)?;
515+
let res = self.check_all_exercises_impl(stdout);
516+
stdout.queue(cursor::Show)?;
517+
518+
res
439519
}
440520

441521
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
@@ -462,20 +542,18 @@ impl AppState {
462542
stdout.write_all(b"\n")?;
463543
}
464544

465-
if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? {
466-
stdout.write_all(b"\n\n")?;
545+
if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
546+
self.set_current_exercise_ind(first_pending_exercise_ind)?;
467547

468-
self.current_exercise_ind = pending_exercise_ind;
469-
self.exercises[pending_exercise_ind].done = false;
470-
// All exercises were marked as done.
471-
self.n_done -= 1;
472-
self.write()?;
473548
return Ok(ExercisesProgress::NewPending);
474549
}
475550

476-
// Write that the last exercise is done.
477-
self.write()?;
551+
self.render_final_message(stdout)?;
552+
553+
Ok(ExercisesProgress::AllDone)
554+
}
478555

556+
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
479557
clear_terminal(stdout)?;
480558
stdout.write_all(FENISH_LINE.as_bytes())?;
481559

@@ -485,15 +563,12 @@ impl AppState {
485563
stdout.write_all(b"\n")?;
486564
}
487565

488-
Ok(ExercisesProgress::AllDone)
566+
Ok(())
489567
}
490568
}
491569

492570
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
493571
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
494-
const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done.
495-
Recompiling and running all exercises to make sure that all of them are actually done.
496-
";
497572
const FENISH_LINE: &str = "+----------------------------------------------------+
498573
| You made it to the Fe-nish line! |
499574
+-------------------------- ------------------------+

0 commit comments

Comments
 (0)