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.
This commit is contained in:
Nahor 2024-10-02 11:45:55 -07:00
parent 0c79f2ea3e
commit 26fd97a209

View file

@ -35,10 +35,12 @@ pub enum StateFileStatus {
NotRead, NotRead,
} }
enum AllExercisesCheck { #[derive(Clone, Copy, PartialEq)]
Pending(usize), enum AllExercisesResult {
AllDone, Pending,
CheckedUntil(usize), Success,
Failed,
Error,
} }
pub struct AppState { pub struct AppState {
@ -270,18 +272,32 @@ 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; Ok(false)
} else {
exercise.done = done;
if done {
self.n_done += 1;
} else {
self.n_done -= 1; self.n_done -= 1;
self.write()?; }
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()?;
}
Ok(()) Ok(())
} }
@ -380,11 +396,11 @@ impl AppState {
} }
// Return the exercise index of the first pending exercise found. // Return the exercise index of the first pending exercise found.
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> { fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.write_all(FINAL_CHECK_MSG)?; stdout.write_all(FINAL_CHECK_MSG)?;
let n_exercises = self.exercises.len(); let n_exercises = self.exercises.len();
let status = thread::scope(|s| { let (mut checked_count, mut results) = thread::scope(|s| {
let handles = self let handles = self
.exercises .exercises
.iter() .iter()
@ -394,48 +410,83 @@ impl AppState {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut results = vec![AllExercisesResult::Pending; n_exercises];
let mut checked_count = 0;
for (exercise_ind, spawn_res) in handles.into_iter().enumerate() { for (exercise_ind, spawn_res) in handles.into_iter().enumerate() {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
stdout.flush()?; stdout.flush()?;
let Ok(handle) = spawn_res else { results[exercise_ind] = spawn_res
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); .context("Spawn error")
}; .and_then(|handle| handle.join().unwrap())
.map_or_else(
let Ok(success) = handle.join().unwrap() else { |_| AllExercisesResult::Error,
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); |success| {
}; checked_count += 1;
if success {
if !success { AllExercisesResult::Success
return Ok(AllExercisesCheck::Pending(exercise_ind)); } else {
AllExercisesResult::Failed
} }
},
);
} }
Ok::<_, io::Error>(AllExercisesCheck::AllDone) Ok::<_, io::Error>((checked_count, results))
})?; })?;
let mut exercise_ind = match status { // If we got an error while checking all exercises in parallel,
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)), // it could be because we exceeded the limit of open file descriptors.
AllExercisesCheck::AllDone => return Ok(None), // Therefore, re-try those one at a time (i.e. sequentially).
AllExercisesCheck::CheckedUntil(ind) => ind, results
.iter_mut()
.enumerate()
.filter(|(_, result)| {
**result == AllExercisesResult::Pending || **result == AllExercisesResult::Error
})
.try_for_each(|(exercise_ind, result)| {
let exercise = self.exercises.get(exercise_ind).context(BAD_INDEX_ERR)?;
*result = match exercise
.run_exercise(None, &self.cmd_runner)
.context("Sequential retry")
{
Ok(true) => AllExercisesResult::Success,
Ok(false) => AllExercisesResult::Failed,
Err(err) => bail!(err),
}; };
checked_count += 1;
// We got an error while checking all exercises in parallel. write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
// This could be because we exceeded the limit of open file descriptors.
// Therefore, try to continue the check sequentially.
for exercise in &self.exercises[exercise_ind..] {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?; stdout.flush()?;
Ok(())
})?;
let success = exercise.run_exercise(None, &self.cmd_runner)?; // Update the state of each exercise and return the first that failed
if !success { let first_fail = results
return Ok(Some(exercise_ind)); .iter()
.enumerate()
.filter_map(|(exercise_ind, result)| {
match result {
AllExercisesResult::Success => self
.set_status(exercise_ind, true)
.map_or_else(|err| Some(Err(err)), |_| None),
AllExercisesResult::Failed => self
.set_status(exercise_ind, false)
.map_or_else(|err| Some(Err(err)), |_| Some(Ok(exercise_ind))),
// The sequential check done earlier will have converted all
// exercises to Success/Failed, or bailed, so those are unreachable
AllExercisesResult::Pending | AllExercisesResult::Error => unreachable!(),
} }
})
exercise_ind += 1; .try_fold(None::<usize>, |current_min, index| {
match (current_min, index) {
(_, Err(err)) => Err(err),
(None, Ok(index)) => Ok(Some(index)),
(Some(current_min), Ok(index)) => Ok(Some(current_min.min(index))),
} }
})?;
self.write()?;
Ok(None) Ok(first_fail)
} }
/// 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.
@ -467,9 +518,7 @@ impl AppState {
self.current_exercise_ind = pending_exercise_ind; self.current_exercise_ind = pending_exercise_ind;
self.exercises[pending_exercise_ind].done = false; 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);
} }