Skip to content

Commit 26fd97a

Browse files
committed
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.
1 parent 0c79f2e commit 26fd97a

File tree

1 file changed

+96
-47
lines changed

1 file changed

+96
-47
lines changed

src/app_state.rs

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ pub enum StateFileStatus {
3535
NotRead,
3636
}
3737

38-
enum AllExercisesCheck {
39-
Pending(usize),
40-
AllDone,
41-
CheckedUntil(usize),
38+
#[derive(Clone, Copy, PartialEq)]
39+
enum AllExercisesResult {
40+
Pending,
41+
Success,
42+
Failed,
43+
Error,
4244
}
4345

4446
pub struct AppState {
@@ -270,18 +272,32 @@ impl AppState {
270272
self.write()
271273
}
272274

273-
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
275+
// Set the status of an exercise without saving. Returns `true` if the
276+
// status actually changed (and thus needs saving later)
277+
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
274278
let exercise = self
275279
.exercises
276280
.get_mut(exercise_ind)
277281
.context(BAD_INDEX_ERR)?;
278282

279-
if exercise.done {
280-
exercise.done = false;
281-
self.n_done -= 1;
282-
self.write()?;
283+
if exercise.done == done {
284+
Ok(false)
285+
} else {
286+
exercise.done = done;
287+
if done {
288+
self.n_done += 1;
289+
} else {
290+
self.n_done -= 1;
291+
}
292+
Ok(true)
283293
}
294+
}
284295

296+
// Set the status of an exercise to "pending" and save
297+
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
298+
if self.set_status(exercise_ind, false)? {
299+
self.write()?;
300+
}
285301
Ok(())
286302
}
287303

@@ -380,11 +396,11 @@ impl AppState {
380396
}
381397

382398
// Return the exercise index of the first pending exercise found.
383-
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
399+
fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
384400
stdout.write_all(FINAL_CHECK_MSG)?;
385401
let n_exercises = self.exercises.len();
386402

387-
let status = thread::scope(|s| {
403+
let (mut checked_count, mut results) = thread::scope(|s| {
388404
let handles = self
389405
.exercises
390406
.iter()
@@ -394,48 +410,83 @@ impl AppState {
394410
})
395411
.collect::<Vec<_>>();
396412

413+
let mut results = vec![AllExercisesResult::Pending; n_exercises];
414+
let mut checked_count = 0;
397415
for (exercise_ind, spawn_res) in handles.into_iter().enumerate() {
398-
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
416+
write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
399417
stdout.flush()?;
400418

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-
}
419+
results[exercise_ind] = spawn_res
420+
.context("Spawn error")
421+
.and_then(|handle| handle.join().unwrap())
422+
.map_or_else(
423+
|_| AllExercisesResult::Error,
424+
|success| {
425+
checked_count += 1;
426+
if success {
427+
AllExercisesResult::Success
428+
} else {
429+
AllExercisesResult::Failed
430+
}
431+
},
432+
);
412433
}
413434

414-
Ok::<_, io::Error>(AllExercisesCheck::AllDone)
435+
Ok::<_, io::Error>((checked_count, results))
415436
})?;
416437

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-
};
422-
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()?;
429-
430-
let success = exercise.run_exercise(None, &self.cmd_runner)?;
431-
if !success {
432-
return Ok(Some(exercise_ind));
433-
}
438+
// If we got an error while checking all exercises in parallel,
439+
// it could be because we exceeded the limit of open file descriptors.
440+
// Therefore, re-try those one at a time (i.e. sequentially).
441+
results
442+
.iter_mut()
443+
.enumerate()
444+
.filter(|(_, result)| {
445+
**result == AllExercisesResult::Pending || **result == AllExercisesResult::Error
446+
})
447+
.try_for_each(|(exercise_ind, result)| {
448+
let exercise = self.exercises.get(exercise_ind).context(BAD_INDEX_ERR)?;
449+
*result = match exercise
450+
.run_exercise(None, &self.cmd_runner)
451+
.context("Sequential retry")
452+
{
453+
Ok(true) => AllExercisesResult::Success,
454+
Ok(false) => AllExercisesResult::Failed,
455+
Err(err) => bail!(err),
456+
};
457+
checked_count += 1;
458+
write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
459+
stdout.flush()?;
460+
Ok(())
461+
})?;
434462

435-
exercise_ind += 1;
436-
}
463+
// Update the state of each exercise and return the first that failed
464+
let first_fail = results
465+
.iter()
466+
.enumerate()
467+
.filter_map(|(exercise_ind, result)| {
468+
match result {
469+
AllExercisesResult::Success => self
470+
.set_status(exercise_ind, true)
471+
.map_or_else(|err| Some(Err(err)), |_| None),
472+
AllExercisesResult::Failed => self
473+
.set_status(exercise_ind, false)
474+
.map_or_else(|err| Some(Err(err)), |_| Some(Ok(exercise_ind))),
475+
// The sequential check done earlier will have converted all
476+
// exercises to Success/Failed, or bailed, so those are unreachable
477+
AllExercisesResult::Pending | AllExercisesResult::Error => unreachable!(),
478+
}
479+
})
480+
.try_fold(None::<usize>, |current_min, index| {
481+
match (current_min, index) {
482+
(_, Err(err)) => Err(err),
483+
(None, Ok(index)) => Ok(Some(index)),
484+
(Some(current_min), Ok(index)) => Ok(Some(current_min.min(index))),
485+
}
486+
})?;
487+
self.write()?;
437488

438-
Ok(None)
489+
Ok(first_fail)
439490
}
440491

441492
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
@@ -467,9 +518,7 @@ impl AppState {
467518

468519
self.current_exercise_ind = pending_exercise_ind;
469520
self.exercises[pending_exercise_ind].done = false;
470-
// All exercises were marked as done.
471-
self.n_done -= 1;
472-
self.write()?;
521+
473522
return Ok(ExercisesProgress::NewPending);
474523
}
475524

0 commit comments

Comments
 (0)