1
- use anyhow:: { bail, Context , Result } ;
1
+ use anyhow:: { bail, Context , Error , Result } ;
2
+ use crossterm:: { cursor, terminal, QueueableCommand } ;
2
3
use std:: {
3
4
env,
4
5
fs:: { File , OpenOptions } ,
5
- io:: { self , Read , Seek , StdoutLock , Write } ,
6
+ io:: { Read , Seek , StdoutLock , Write } ,
6
7
path:: { Path , MAIN_SEPARATOR_STR } ,
7
8
process:: { Command , Stdio } ,
9
+ sync:: {
10
+ atomic:: { AtomicUsize , Ordering :: Relaxed } ,
11
+ mpsc,
12
+ } ,
8
13
thread,
9
14
} ;
10
15
@@ -15,10 +20,11 @@ use crate::{
15
20
embedded:: EMBEDDED_FILES ,
16
21
exercise:: { Exercise , RunnableExercise } ,
17
22
info_file:: ExerciseInfo ,
18
- term,
23
+ term:: { self , CheckProgressVisualizer } ,
19
24
} ;
20
25
21
26
const STATE_FILE_NAME : & str = ".rustlings-state.txt" ;
27
+ const DEFAULT_CHECK_PARALLELISM : usize = 8 ;
22
28
23
29
#[ must_use]
24
30
pub enum ExercisesProgress {
@@ -35,10 +41,12 @@ pub enum StateFileStatus {
35
41
NotRead ,
36
42
}
37
43
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 ,
42
50
}
43
51
44
52
pub struct AppState {
@@ -194,6 +202,11 @@ impl AppState {
194
202
self . n_done
195
203
}
196
204
205
+ #[ inline]
206
+ pub fn n_pending ( & self ) -> u16 {
207
+ self . exercises . len ( ) as u16 - self . n_done
208
+ }
209
+
197
210
#[ inline]
198
211
pub fn current_exercise ( & self ) -> & Exercise {
199
212
& self . exercises [ self . current_exercise_ind ]
@@ -270,15 +283,31 @@ impl AppState {
270
283
self . write ( )
271
284
}
272
285
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 > {
274
289
let exercise = self
275
290
. exercises
276
291
. get_mut ( exercise_ind)
277
292
. context ( BAD_INDEX_ERR ) ?;
278
293
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 {
281
302
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 ) ? {
282
311
self . write ( ) ?;
283
312
}
284
313
@@ -379,63 +408,114 @@ impl AppState {
379
408
}
380
409
}
381
410
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" ) ?;
412
459
}
413
460
414
- Ok :: < _ , io :: Error > ( AllExercisesCheck :: AllDone )
415
- } ) ? ;
461
+ // Drop this sender to detect when the last thread is done.
462
+ drop ( exercise_progress_sender ) ;
416
463
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
+ }
422
468
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
+ } ) ?;
429
471
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
+ }
433
504
}
434
-
435
- exercise_ind += 1 ;
436
505
}
437
506
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
439
519
}
440
520
441
521
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
@@ -462,20 +542,18 @@ impl AppState {
462
542
stdout. write_all ( b"\n " ) ?;
463
543
}
464
544
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 ) ?;
467
547
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 ( ) ?;
473
548
return Ok ( ExercisesProgress :: NewPending ) ;
474
549
}
475
550
476
- // Write that the last exercise is done.
477
- self . write ( ) ?;
551
+ self . render_final_message ( stdout) ?;
552
+
553
+ Ok ( ExercisesProgress :: AllDone )
554
+ }
478
555
556
+ pub fn render_final_message ( & self , stdout : & mut StdoutLock ) -> Result < ( ) > {
479
557
clear_terminal ( stdout) ?;
480
558
stdout. write_all ( FENISH_LINE . as_bytes ( ) ) ?;
481
559
@@ -485,15 +563,12 @@ impl AppState {
485
563
stdout. write_all ( b"\n " ) ?;
486
564
}
487
565
488
- Ok ( ExercisesProgress :: AllDone )
566
+ Ok ( ( ) )
489
567
}
490
568
}
491
569
492
570
const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
493
571
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
- " ;
497
572
const FENISH_LINE : & str = "+----------------------------------------------------+
498
573
| You made it to the Fe-nish line! |
499
574
+-------------------------- ------------------------+
0 commit comments