diff options
| author | marisa <mokou@posteo.de> | 2019-11-11 17:21:06 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-11-11 17:21:06 +0100 |
| commit | ec2d4bd3ee665f2a4c79dd42c41078223074d4c1 (patch) | |
| tree | 6106b922559491112240f6465f965cf811caf5b8 /src | |
| parent | ce9fa6ebbfdc3e7585d488d9409797285708316f (diff) | |
| parent | 9a9007abae86c3b1b1c09778a6544ced54ea4453 (diff) | |
Merge branch 'master' into refactor-hints
Diffstat (limited to 'src')
| -rw-r--r-- | src/exercise.rs | 113 | ||||
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/run.rs | 4 | ||||
| -rw-r--r-- | src/verify.rs | 56 |
4 files changed, 165 insertions, 12 deletions
diff --git a/src/exercise.rs b/src/exercise.rs index 0e8a199..be59284 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,16 +1,20 @@ +use regex::Regex; use serde::Deserialize; use std::fmt::{self, Display, Formatter}; -use std::fs::remove_file; +use std::fs::{remove_file, File}; +use std::io::Read; use std::path::PathBuf; use std::process::{self, Command, Output}; const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; +const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE"; +const CONTEXT: usize = 2; fn temp_file() -> String { format!("./temp_{}", process::id()) } -#[derive(Deserialize)] +#[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum Mode { Compile, @@ -30,6 +34,19 @@ pub struct Exercise { pub hint: String, } +#[derive(PartialEq, Debug)] +pub enum State { + Done, + Pending(Vec<ContextLine>), +} + +#[derive(PartialEq, Debug)] +pub struct ContextLine { + pub line: String, + pub number: usize, + pub important: bool, +} + impl Exercise { pub fn compile(&self) -> Output { match self.mode { @@ -54,6 +71,48 @@ impl Exercise { pub fn clean(&self) { let _ignored = remove_file(&temp_file()); } + + pub fn state(&self) -> State { + let mut source_file = + File::open(&self.path).expect("We were unable to open the exercise file!"); + + let source = { + let mut s = String::new(); + source_file + .read_to_string(&mut s) + .expect("We were unable to read the exercise file!"); + s + }; + + let re = Regex::new(I_AM_DONE_REGEX).unwrap(); + + if !re.is_match(&source) { + return State::Done; + } + + let matched_line_index = source + .lines() + .enumerate() + .filter_map(|(i, line)| if re.is_match(line) { Some(i) } else { None }) + .next() + .expect("This should not happen at all"); + + let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize; + let max_line = matched_line_index + CONTEXT; + + let context = source + .lines() + .enumerate() + .filter(|&(i, _)| i >= min_line && i <= max_line) + .map(|(i, line)| ContextLine { + line: line.to_string(), + number: i + 1, + important: i == matched_line_index, + }) + .collect(); + + State::Pending(context) + } } impl Display for Exercise { @@ -65,7 +124,6 @@ impl Display for Exercise { #[cfg(test)] mod test { use super::*; - use std::fs::File; use std::path::Path; #[test] @@ -80,4 +138,53 @@ mod test { exercise.clean(); assert!(!Path::new(&temp_file()).exists()); } + + #[test] + fn test_pending_state() { + let exercise = Exercise { + path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + mode: Mode::Compile, + }; + + let state = exercise.state(); + let expected = vec![ + ContextLine { + line: "// fake_exercise".to_string(), + number: 1, + important: false, + }, + ContextLine { + line: "".to_string(), + number: 2, + important: false, + }, + ContextLine { + line: "// I AM NOT DONE".to_string(), + number: 3, + important: true, + }, + ContextLine { + line: "".to_string(), + number: 4, + important: false, + }, + ContextLine { + line: "fn main() {".to_string(), + number: 5, + important: false, + }, + ]; + + assert_eq!(state, State::Pending(expected)); + } + + #[test] + fn test_finished_exercise() { + let exercise = Exercise { + path: PathBuf::from("tests/fixture/state/finished_exercise.rs"), + mode: Mode::Compile, + }; + + assert_eq!(exercise.state(), State::Done); + } } diff --git a/src/main.rs b/src/main.rs index 875f767..5a4af53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,11 +127,11 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> { DebouncedEvent::Create(b) | DebouncedEvent::Chmod(b) | DebouncedEvent::Write(b) => { if b.extension() == Some(OsStr::new("rs")) && b.exists() { let filepath = b.as_path().canonicalize().unwrap(); - let exercise = exercises + let pending_exercises = exercises .iter() .skip_while(|e| !filepath.ends_with(&e.path)); clear_screen(); - let _ignored = verify(exercise); + let _ignored = verify(pending_exercises); } } _ => {} @@ -5,7 +5,9 @@ use indicatif::ProgressBar; pub fn run(exercise: &Exercise) -> Result<(), ()> { match exercise.mode { - Mode::Test => test(exercise)?, + Mode::Test => { + test(exercise)?; + } Mode::Compile => compile_and_run(exercise)?, } Ok(()) diff --git a/src/verify.rs b/src/verify.rs index d066afa..020102e 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,18 +1,21 @@ -use crate::exercise::{Exercise, Mode}; +use crate::exercise::{ContextLine, Exercise, Mode, State}; use console::{style, Emoji}; use indicatif::ProgressBar; pub fn verify<'a>(start_at: impl IntoIterator<Item = &'a Exercise>) -> Result<(), ()> { for exercise in start_at { - match exercise.mode { + let is_done = match exercise.mode { Mode::Test => test(&exercise)?, Mode::Compile => compile_only(&exercise)?, + }; + if !is_done { + return Err(()); } } Ok(()) } -fn compile_only(exercise: &Exercise) -> Result<(), ()> { +fn compile_only(exercise: &Exercise) -> Result<bool, ()> { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); progress_bar.enable_steady_tick(100); @@ -22,7 +25,12 @@ fn compile_only(exercise: &Exercise) -> Result<(), ()> { let formatstr = format!("{} Successfully compiled {}!", Emoji("✅", "✓"), exercise); println!("{}", style(formatstr).green()); exercise.clean(); - Ok(()) + if let State::Pending(context) = exercise.state() { + print_everything_looks_good(exercise.mode, context); + Ok(false) + } else { + Ok(true) + } } else { let formatstr = format!( "{} Compilation of {} failed! Compiler error message:\n", @@ -36,7 +44,7 @@ fn compile_only(exercise: &Exercise) -> Result<(), ()> { } } -pub fn test(exercise: &Exercise) -> Result<(), ()> { +pub fn test(exercise: &Exercise) -> Result<bool, ()> { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Testing {}...", exercise).as_str()); progress_bar.enable_steady_tick(100); @@ -52,7 +60,12 @@ pub fn test(exercise: &Exercise) -> Result<(), ()> { let formatstr = format!("{} Successfully tested {}!", Emoji("✅", "✓"), exercise); println!("{}", style(formatstr).green()); exercise.clean(); - Ok(()) + if let State::Pending(context) = exercise.state() { + print_everything_looks_good(exercise.mode, context); + Ok(false) + } else { + Ok(true) + } } else { let formatstr = format!( "{} Testing of {} failed! Please try again. Here's the output:", @@ -77,3 +90,34 @@ pub fn test(exercise: &Exercise) -> Result<(), ()> { Err(()) } } + +fn print_everything_looks_good(mode: Mode, context: Vec<ContextLine>) { + let success_msg = match mode { + Mode::Compile => "The code is compiling!", + Mode::Test => "The code is compiling, and the tests pass!", + }; + + println!(""); + println!("🎉 🎉 {} 🎉 🎉", success_msg); + println!(""); + println!("You can keep working on this exercise,"); + println!( + "or jump into the next one by removing the {} comment:", + style("`I AM NOT DONE`").bold() + ); + println!(""); + for context_line in context { + let formatted_line = if context_line.important { + format!("{}", style(context_line.line).bold()) + } else { + format!("{}", context_line.line) + }; + + println!( + "{:>2} {} {}", + style(context_line.number).blue().bold(), + style("|").blue(), + formatted_line + ); + } +} |
