diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index bd73195a..d75d73fc 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -31,6 +31,7 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md""" name = "intro1" dir = "00_intro" test = false +skip_check_unsolved = true hint = """ Enter `n` to move on to the next exercise. You might need to press ENTER after typing `n`.""" diff --git a/src/dev/check.rs b/src/dev/check.rs index 29b2b670..5c35462c 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -162,7 +162,46 @@ fn check_unexpected_files( Ok(()) } -fn check_exercises(info_file: &InfoFile) -> Result<()> { +fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> { + let error_occurred = AtomicBool::new(false); + + println!( + "Running all exercises to check that they aren't already solved. This may take a while…\n", + ); + thread::scope(|s| { + for exercise_info in &info_file.exercises { + if exercise_info.skip_check_unsolved { + continue; + } + + s.spawn(|| { + let error = |e| { + let mut stderr = io::stderr().lock(); + stderr.write_all(e).unwrap(); + stderr.write_all(b"\nProblem with the exercise ").unwrap(); + stderr.write_all(exercise_info.name.as_bytes()).unwrap(); + stderr.write_all(SEPARATOR).unwrap(); + error_occurred.store(true, atomic::Ordering::Relaxed); + }; + + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + match exercise_info.run_exercise(&mut output, target_dir) { + Ok(true) => error(b"Already solved!"), + Ok(false) => (), + Err(e) => error(e.to_string().as_bytes()), + } + }); + } + }); + + if error_occurred.load(atomic::Ordering::Relaxed) { + bail!(CHECK_EXERCISES_UNSOLVED_ERR); + } + + Ok(()) +} + +fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), @@ -172,15 +211,14 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { let info_file_paths = check_info_file_exercises(info_file)?; check_unexpected_files("exercises", &info_file_paths)?; - Ok(()) + check_exercises_unsolved(info_file, target_dir) } -fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> { - let target_dir = parse_target_dir()?; +fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &Path) -> Result<()> { let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len())); let error_occurred = AtomicBool::new(false); - println!("Running all solutions. This may take a while...\n"); + println!("Running all solutions. This may take a while…\n"); thread::scope(|s| { for exercise_info in &info_file.exercises { s.spawn(|| { @@ -206,7 +244,7 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> } let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - match exercise_info.run_solution(&mut output, &target_dir) { + match exercise_info.run_solution(&mut output, target_dir) { Ok(true) => { paths.lock().unwrap().insert(PathBuf::from(path)); } @@ -242,8 +280,9 @@ pub fn check(require_solutions: bool) -> Result<()> { check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; } - check_exercises(&info_file)?; - check_solutions(require_solutions, &info_file)?; + let target_dir = parse_target_dir()?; + check_exercises(&info_file, &target_dir)?; + check_solutions(require_solutions, &info_file, &target_dir)?; println!("\nEverything looks fine!"); @@ -252,3 +291,6 @@ pub fn check(require_solutions: bool) -> Result<()> { const SEPARATOR: &[u8] = b"\n========================================================================================\n"; + +const CHECK_EXERCISES_UNSOLVED_ERR: &str = "At least one exercise is already solved or failed to run. See the output above. +If this is an intro exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file."; diff --git a/src/info_file.rs b/src/info_file.rs index f226f735..f27d0185 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -11,14 +11,17 @@ pub struct ExerciseInfo { pub name: String, /// Exercise's directory name inside the `exercises/` directory. pub dir: Option, - #[serde(default = "default_true")] /// Run `cargo test` on the exercise. + #[serde(default = "default_true")] pub test: bool, /// Deny all Clippy warnings. #[serde(default)] pub strict_clippy: bool, /// The exercise's hint to be shown to the user on request. pub hint: String, + /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. + #[serde(default)] + pub skip_check_unsolved: bool, } #[inline(always)] const fn default_true() -> bool {