1- use anyhow:: { bail, Context , Result } ;
1+ use anyhow:: { bail, Context , Error , Result } ;
2+ use crossterm:: { cursor, terminal, QueueableCommand } ;
23use 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
2126const STATE_FILE_NAME : & str = ".rustlings-state.txt" ;
27+ const DEFAULT_CHECK_PARALLELISM : usize = 8 ;
2228
2329#[ must_use]
2430pub 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
4452pub 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, "\r Progress: {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, "\r Progress: {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
492570const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
493571const 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- " ;
497572const FENISH_LINE : & str = "+----------------------------------------------------+
498573| You made it to the Fe-nish line! |
499574+-------------------------- ------------------------+
0 commit comments