feat: improve `watch` execution mode

The `watch` command now requires user action to move to the next
exercise.

BREAKING CHANGE: this changes the behavior of `watch`.
This commit is contained in:
Roberto Vidal 2019-11-11 13:38:24 +01:00
parent a47a62172a
commit 2cdd61294f
60 changed files with 309 additions and 12 deletions

8
Cargo.lock generated
View File

@ -204,6 +204,11 @@ name = "fuchsia-zircon-sys"
version = "0.3.3" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "indicatif" name = "indicatif"
version = "0.9.0" version = "0.9.0"
@ -605,8 +610,10 @@ dependencies = [
"assert_cmd 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "assert_cmd 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"console 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "console 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"indicatif 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "indicatif 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -848,6 +855,7 @@ dependencies = [
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
"checksum indicatif 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a29b2fa6f00010c268bface64c18bb0310aaa70d46a195d5382d288c477fb016" "checksum indicatif 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a29b2fa6f00010c268bface64c18bb0310aaa70d46a195d5382d288c477fb016"
"checksum inotify 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40b54539f3910d6f84fbf9a643efd6e3aa6e4f001426c0329576128255994718" "checksum inotify 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40b54539f3910d6f84fbf9a643efd6e3aa6e4f001426c0329576128255994718"
"checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" "checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0"

View File

@ -10,6 +10,7 @@ indicatif = "0.9.0"
console = "0.6.2" console = "0.6.2"
notify = "4.0.0" notify = "4.0.0"
toml = "0.4.10" toml = "0.4.10"
regex = "1.1.6"
serde = {version = "1.0.10", features = ["derive"]} serde = {version = "1.0.10", features = ["derive"]}
[[bin]] [[bin]]
@ -18,3 +19,4 @@ path = "src/main.rs"
[dev-dependencies] [dev-dependencies]
assert_cmd = "0.11.0" assert_cmd = "0.11.0"
glob = "0.3.0"

View File

@ -1,6 +1,8 @@
// enums1.rs // enums1.rs
// Make me compile! Scroll down for hints! // Make me compile! Scroll down for hints!
// I AM NOT DONE
#[derive(Debug)] #[derive(Debug)]
enum Message { enum Message {
// TODO: define a few types of messages as used below // TODO: define a few types of messages as used below

View File

@ -1,6 +1,8 @@
// enums2.rs // enums2.rs
// Make me compile! Scroll down for hints // Make me compile! Scroll down for hints
// I AM NOT DONE
#[derive(Debug)] #[derive(Debug)]
enum Message { enum Message {
// TODO: define the different variants used below // TODO: define the different variants used below

View File

@ -1,6 +1,8 @@
// enums3.rs // enums3.rs
// Address all the TODOs to make the tests pass! // Address all the TODOs to make the tests pass!
// I AM NOT DONE
enum Message { enum Message {
// TODO: implement the message variant types based on their usage below // TODO: implement the message variant types based on their usage below
} }

View File

@ -6,6 +6,8 @@
// this function to have. // this function to have.
// Scroll down for hints!!! // Scroll down for hints!!!
// I AM NOT DONE
pub fn generate_nametag_text(name: String) -> Option<String> { pub fn generate_nametag_text(name: String) -> Option<String> {
if name.len() > 0 { if name.len() > 0 {
Some(format!("Hi! My name is {}", name)) Some(format!("Hi! My name is {}", name))

View File

@ -16,6 +16,8 @@
// There are at least two ways to implement this that are both correct-- but // There are at least two ways to implement this that are both correct-- but
// one is a lot shorter! Scroll down for hints to both ways. // one is a lot shorter! Scroll down for hints to both ways.
// I AM NOT DONE
use std::num::ParseIntError; use std::num::ParseIntError;
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {

View File

@ -3,6 +3,8 @@
// `total_cost` function from the previous exercise. It's not working though! // `total_cost` function from the previous exercise. It's not working though!
// Why not? What should we do to fix it? Scroll for hints! // Why not? What should we do to fix it? Scroll for hints!
// I AM NOT DONE
use std::num::ParseIntError; use std::num::ParseIntError;
fn main() { fn main() {

View File

@ -15,6 +15,8 @@
// //
// Scroll down for hints :) // Scroll down for hints :)
// I AM NOT DONE
use std::error; use std::error;
use std::fmt; use std::fmt;
use std::io; use std::io;

View File

@ -4,6 +4,8 @@
// on `None`. Handle this in a more graceful way than calling `unwrap`! // on `None`. Handle this in a more graceful way than calling `unwrap`!
// Scroll down for hints :) // Scroll down for hints :)
// I AM NOT DONE
pub fn pop_too_much() -> bool { pub fn pop_too_much() -> bool {
let mut list = vec![3]; let mut list = vec![3];

View File

@ -1,6 +1,8 @@
// result1.rs // result1.rs
// Make this test pass! Scroll down for hints :) // Make this test pass! Scroll down for hints :)
// I AM NOT DONE
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64); struct PositiveNonzeroInteger(u64);

View File

@ -1,6 +1,8 @@
// functions1.rs // functions1.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
call_me(); call_me();
} }

View File

@ -1,6 +1,8 @@
// functions2.rs // functions2.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
call_me(3); call_me(3);
} }

View File

@ -1,6 +1,8 @@
// functions3.rs // functions3.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
call_me(); call_me();
} }

View File

@ -4,6 +4,8 @@
// This store is having a sale where if the price is an even number, you get // This store is having a sale where if the price is an even number, you get
// 10 (money unit) off, but if it's an odd number, it's 3 (money unit) less. // 10 (money unit) off, but if it's an odd number, it's 3 (money unit) less.
// I AM NOT DONE
fn main() { fn main() {
let original_price = 51; let original_price = 51;
println!("Your sale price is {}", sale_price(original_price)); println!("Your sale price is {}", sale_price(original_price));

View File

@ -1,6 +1,8 @@
// functions5.rs // functions5.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let answer = square(3); let answer = square(3);
println!("The answer is {}", answer); println!("The answer is {}", answer);

View File

@ -1,5 +1,7 @@
// if1.rs // if1.rs
// I AM NOT DONE
pub fn bigger(a: i32, b: i32) -> i32 { pub fn bigger(a: i32, b: i32) -> i32 {
// Complete this function to return the bigger number! // Complete this function to return the bigger number!
// Do not use: // Do not use:

View File

@ -1,6 +1,8 @@
// macros1.rs // macros1.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
macro_rules! my_macro { macro_rules! my_macro {
() => { () => {
println!("Check out my macro!"); println!("Check out my macro!");

View File

@ -1,6 +1,8 @@
// macros2.rs // macros2.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
my_macro!(); my_macro!();
} }

View File

@ -1,6 +1,8 @@
// macros3.rs // macros3.rs
// Make me compile, without taking the macro out of the module! Scroll down for hints :) // Make me compile, without taking the macro out of the module! Scroll down for hints :)
// I AM NOT DONE
mod macros { mod macros {
macro_rules! my_macro { macro_rules! my_macro {
() => { () => {

View File

@ -1,6 +1,8 @@
// macros4.rs // macros4.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
macro_rules! my_macro { macro_rules! my_macro {
() => { () => {
println!("Check out my macro!"); println!("Check out my macro!");

View File

@ -1,6 +1,8 @@
// modules1.rs // modules1.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
mod sausage_factory { mod sausage_factory {
fn make_sausage() { fn make_sausage() {
println!("sausage!"); println!("sausage!");

View File

@ -1,6 +1,8 @@
// modules2.rs // modules2.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
mod delicious_snacks { mod delicious_snacks {
use self::fruits::PEAR as fruit; use self::fruits::PEAR as fruit;
use self::veggies::CUCUMBER as veggie; use self::veggies::CUCUMBER as veggie;

View File

@ -1,6 +1,8 @@
// move_semantics1.rs // move_semantics1.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let vec0 = Vec::new(); let vec0 = Vec::new();

View File

@ -1,6 +1,8 @@
// move_semantics2.rs // move_semantics2.rs
// Make me compile without changing line 10! Scroll down for hints :) // Make me compile without changing line 10! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let vec0 = Vec::new(); let vec0 = Vec::new();

View File

@ -3,6 +3,8 @@
// (no lines with multiple semicolons necessary!) // (no lines with multiple semicolons necessary!)
// Scroll down for hints :) // Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let vec0 = Vec::new(); let vec0 = Vec::new();

View File

@ -3,6 +3,8 @@
// in `fn main`, we instead create it within `fn fill_vec` and transfer the // in `fn main`, we instead create it within `fn fill_vec` and transfer the
// freshly created vector from fill_vec to its caller. Scroll for hints! // freshly created vector from fill_vec to its caller. Scroll for hints!
// I AM NOT DONE
fn main() { fn main() {
let vec0 = Vec::new(); let vec0 = Vec::new();

View File

@ -2,6 +2,8 @@
// Fill in the rest of the line that has code missing! // Fill in the rest of the line that has code missing!
// No hints, there's no tricks, just get used to typing these :) // No hints, there's no tricks, just get used to typing these :)
// I AM NOT DONE
fn main() { fn main() {
// Booleans (`bool`) // Booleans (`bool`)

View File

@ -2,6 +2,8 @@
// Fill in the rest of the line that has code missing! // Fill in the rest of the line that has code missing!
// No hints, there's no tricks, just get used to typing these :) // No hints, there's no tricks, just get used to typing these :)
// I AM NOT DONE
fn main() { fn main() {
// Characters (`char`) // Characters (`char`)

View File

@ -2,6 +2,8 @@
// Create an array with at least 100 elements in it where the ??? is. // Create an array with at least 100 elements in it where the ??? is.
// Scroll down for hints! // Scroll down for hints!
// I AM NOT DONE
fn main() { fn main() {
let a = ??? let a = ???

View File

@ -2,6 +2,8 @@
// Get a slice out of Array a where the ??? is so that the `if` statement // Get a slice out of Array a where the ??? is so that the `if` statement
// returns true. Scroll down for hints!! // returns true. Scroll down for hints!!
// I AM NOT DONE
#[test] #[test]
fn main() { fn main() {
let a = [1, 2, 3, 4, 5]; let a = [1, 2, 3, 4, 5];

View File

@ -2,6 +2,8 @@
// Destructure the `cat` tuple so that the println will work. // Destructure the `cat` tuple so that the println will work.
// Scroll down for hints! // Scroll down for hints!
// I AM NOT DONE
fn main() { fn main() {
let cat = ("Furry McFurson", 3.5); let cat = ("Furry McFurson", 3.5);
let /* your pattern here */ = cat; let /* your pattern here */ = cat;

View File

@ -3,6 +3,8 @@
// You can put this right into the `println!` where the ??? is. // You can put this right into the `println!` where the ??? is.
// Scroll down for hints! // Scroll down for hints!
// I AM NOT DONE
fn main() { fn main() {
let numbers = (1, 2, 3); let numbers = (1, 2, 3);
println!("The second number is {}", ???); println!("The second number is {}", ???);

View File

@ -4,6 +4,8 @@
// somewhere. Try not to create any copies of the `numbers` Vec! // somewhere. Try not to create any copies of the `numbers` Vec!
// Scroll down for hints :) // Scroll down for hints :)
// I AM NOT DONE
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;

View File

@ -5,6 +5,8 @@
// Step 3. Apply the `capitalize_first` function again to a list, but try and ensure it returns a single string // Step 3. Apply the `capitalize_first` function again to a list, but try and ensure it returns a single string
// As always, there are hints below! // As always, there are hints below!
// I AM NOT DONE
pub fn capitalize_first(input: &str) -> String { pub fn capitalize_first(input: &str) -> String {
let mut c = input.chars(); let mut c = input.chars();
match c.next() { match c.next() {

View File

@ -8,6 +8,8 @@
// a major hint. // a major hint.
// Have fun :-) // Have fun :-)
// I AM NOT DONE
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum DivisionError { pub enum DivisionError {
NotDivisible(NotDivisibleError), NotDivisible(NotDivisibleError),

View File

@ -1,5 +1,7 @@
// iterators4.rs // iterators4.rs
// I AM NOT DONE
pub fn factorial(num: u64) -> u64 { pub fn factorial(num: u64) -> u64 {
// Complete this function to return factorial of num // Complete this function to return factorial of num
// Do not use: // Do not use:

View File

@ -1,6 +1,8 @@
// strings1.rs // strings1.rs
// Make me compile without changing the function signature! Scroll down for hints :) // Make me compile without changing the function signature! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let answer = current_favorite_color(); let answer = current_favorite_color();
println!("My current favorite color is {}", answer); println!("My current favorite color is {}", answer);

View File

@ -1,6 +1,8 @@
// strings2.rs // strings2.rs
// Make me compile without changing the function signature! Scroll down for hints :) // Make me compile without changing the function signature! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let word = String::from("green"); // Try not changing this line :) let word = String::from("green"); // Try not changing this line :)
if is_a_color_word(word) { if is_a_color_word(word) {

View File

@ -1,6 +1,8 @@
// structs1.rs // structs1.rs
// Address all the TODOs to make the tests pass! // Address all the TODOs to make the tests pass!
// I AM NOT DONE
struct ColorClassicStruct { struct ColorClassicStruct {
// TODO: Something goes here // TODO: Something goes here
} }

View File

@ -2,6 +2,8 @@
// Address all the TODOs to make the tests pass! // Address all the TODOs to make the tests pass!
// No hints, just do it! // No hints, just do it!
// I AM NOT DONE
#[derive(Debug)] #[derive(Debug)]
struct Order { struct Order {
name: String, name: String,

View File

@ -7,6 +7,8 @@
// more than 40 at once, each apple only costs 1! Write a function that calculates // more than 40 at once, each apple only costs 1! Write a function that calculates
// the price of an order of apples given the order amount. No hints this time! // the price of an order of apples given the order amount. No hints this time!
// I AM NOT DONE
// Put your function here! // Put your function here!
// fn ..... { // fn ..... {

View File

@ -7,6 +7,8 @@
// you think each value is. That is, add either `string_slice` or `string` // you think each value is. That is, add either `string_slice` or `string`
// before the parentheses on each line. If you're right, it will compile! // before the parentheses on each line. If you're right, it will compile!
// I AM NOT DONE
fn string_slice(arg: &str) { fn string_slice(arg: &str) {
println!("{}", arg); println!("{}", arg);
} }

View File

@ -7,6 +7,8 @@
// we expect to get when we call `times_two` with a negative number. // we expect to get when we call `times_two` with a negative number.
// No hints, you can do this :) // No hints, you can do this :)
// I AM NOT DONE
pub fn times_two(num: i32) -> i32 { pub fn times_two(num: i32) -> i32 {
num * 2 num * 2
} }

View File

@ -5,6 +5,8 @@
// Write a macro that passes the test! No hints this time, you can do it! // Write a macro that passes the test! No hints this time, you can do it!
// I AM NOT DONE
fn main() { fn main() {
if my_macro!("world!") != "Hello world!" { if my_macro!("world!") != "Hello world!" {
panic!("Oh no! Wrong output!"); panic!("Oh no! Wrong output!");

View File

@ -6,6 +6,8 @@
// This test has a problem with it -- make the test compile! Make the test // This test has a problem with it -- make the test compile! Make the test
// pass! Make the test fail! Scroll down for hints :) // pass! Make the test fail! Scroll down for hints :)
// I AM NOT DONE
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test] #[test]

View File

@ -2,6 +2,8 @@
// This test has a problem with it -- make the test compile! Make the test // This test has a problem with it -- make the test compile! Make the test
// pass! Make the test fail! Scroll down for hints :) // pass! Make the test fail! Scroll down for hints :)
// I AM NOT DONE
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test] #[test]

View File

@ -3,6 +3,8 @@
// the test passes. Then write a second test that tests whether we get the result // the test passes. Then write a second test that tests whether we get the result
// we expect to get when we call `is_even(5)`. Scroll down for hints! // we expect to get when we call `is_even(5)`. Scroll down for hints!
// I AM NOT DONE
pub fn is_even(num: i32) -> bool { pub fn is_even(num: i32) -> bool {
num % 2 == 0 num % 2 == 0
} }

View File

@ -5,6 +5,8 @@
// of "waiting..." and the program ends without timing out the playground, // of "waiting..." and the program ends without timing out the playground,
// you've got it :) // you've got it :)
// I AM NOT DONE
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;

View File

@ -1,6 +1,13 @@
// variables1.rs // variables1.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// About this `I AM NOT DONE` thing:
// We sometimes encourage you to keep trying things on a given exercise,
// even after you already figured it out. If you got everything working and
// feel ready for the next exercise, you the `I AM NOT DONE` comment below.
// I AM NOT DONE
fn main() { fn main() {
x = 5; x = 5;
println!("x has the value {}", x); println!("x has the value {}", x);

View File

@ -1,6 +1,8 @@
// variables2.rs // variables2.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let x; let x;
if x == 10 { if x == 10 {

View File

@ -1,6 +1,8 @@
// variables3.rs // variables3.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let x = 3; let x = 3;
println!("Number {}", x); println!("Number {}", x);

View File

@ -1,6 +1,8 @@
// variables4.rs // variables4.rs
// Make me compile! Scroll down for hints :) // Make me compile! Scroll down for hints :)
// I AM NOT DONE
fn main() { fn main() {
let x: i32; let x: i32;
println!("Number {}", x); println!("Number {}", x);

View File

@ -1,16 +1,20 @@
use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::{self, Display, Formatter}; 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::path::PathBuf;
use std::process::{self, Command, Output}; use std::process::{self, Command, Output};
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; 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 { fn temp_file() -> String {
format!("./temp_{}", process::id()) format!("./temp_{}", process::id())
} }
#[derive(Deserialize)] #[derive(Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Mode { pub enum Mode {
Compile, Compile,
@ -28,6 +32,19 @@ pub struct Exercise {
pub mode: Mode, pub mode: Mode,
} }
#[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 { impl Exercise {
pub fn compile(&self) -> Output { pub fn compile(&self) -> Output {
match self.mode { match self.mode {
@ -52,6 +69,48 @@ impl Exercise {
pub fn clean(&self) { pub fn clean(&self) {
let _ignored = remove_file(&temp_file()); 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 { impl Display for Exercise {
@ -63,7 +122,6 @@ impl Display for Exercise {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use std::fs::File;
use std::path::Path; use std::path::Path;
#[test] #[test]
@ -76,4 +134,53 @@ mod test {
exercise.clean(); exercise.clean();
assert!(!Path::new(&temp_file()).exists()); 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);
}
} }

View File

@ -110,11 +110,11 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> {
DebouncedEvent::Create(b) | DebouncedEvent::Chmod(b) | DebouncedEvent::Write(b) => { DebouncedEvent::Create(b) | DebouncedEvent::Chmod(b) | DebouncedEvent::Write(b) => {
if b.extension() == Some(OsStr::new("rs")) && b.exists() { if b.extension() == Some(OsStr::new("rs")) && b.exists() {
let filepath = b.as_path().canonicalize().unwrap(); let filepath = b.as_path().canonicalize().unwrap();
let exercise = exercises let pending_exercises = exercises
.iter() .iter()
.skip_while(|e| !filepath.ends_with(&e.path)); .skip_while(|e| !filepath.ends_with(&e.path));
clear_screen(); clear_screen();
let _ignored = verify(exercise); let _ignored = verify(pending_exercises);
} }
} }
_ => {} _ => {}

View File

@ -5,7 +5,9 @@ use indicatif::ProgressBar;
pub fn run(exercise: &Exercise) -> Result<(), ()> { pub fn run(exercise: &Exercise) -> Result<(), ()> {
match exercise.mode { match exercise.mode {
Mode::Test => test(exercise)?, Mode::Test => {
test(exercise)?;
}
Mode::Compile => compile_and_run(exercise)?, Mode::Compile => compile_and_run(exercise)?,
} }
Ok(()) Ok(())

View File

@ -1,18 +1,21 @@
use crate::exercise::{Exercise, Mode}; use crate::exercise::{ContextLine, Exercise, Mode, State};
use console::{style, Emoji}; use console::{style, Emoji};
use indicatif::ProgressBar; use indicatif::ProgressBar;
pub fn verify<'a>(start_at: impl IntoIterator<Item = &'a Exercise>) -> Result<(), ()> { pub fn verify<'a>(start_at: impl IntoIterator<Item = &'a Exercise>) -> Result<(), ()> {
for exercise in start_at { for exercise in start_at {
match exercise.mode { let is_done = match exercise.mode {
Mode::Test => test(&exercise)?, Mode::Test => test(&exercise)?,
Mode::Compile => compile_only(&exercise)?, Mode::Compile => compile_only(&exercise)?,
};
if !is_done {
return Err(());
} }
} }
Ok(()) Ok(())
} }
fn compile_only(exercise: &Exercise) -> Result<(), ()> { fn compile_only(exercise: &Exercise) -> Result<bool, ()> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); progress_bar.set_message(format!("Compiling {}...", exercise).as_str());
progress_bar.enable_steady_tick(100); progress_bar.enable_steady_tick(100);
@ -22,7 +25,12 @@ fn compile_only(exercise: &Exercise) -> Result<(), ()> {
let formatstr = format!("{} Successfully compiled {}!", Emoji("", ""), exercise); let formatstr = format!("{} Successfully compiled {}!", Emoji("", ""), exercise);
println!("{}", style(formatstr).green()); println!("{}", style(formatstr).green());
exercise.clean(); exercise.clean();
Ok(()) if let State::Pending(context) = exercise.state() {
print_everything_looks_good(exercise.mode, context);
Ok(false)
} else {
Ok(true)
}
} else { } else {
let formatstr = format!( let formatstr = format!(
"{} Compilation of {} failed! Compiler error message:\n", "{} 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(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Testing {}...", exercise).as_str()); progress_bar.set_message(format!("Testing {}...", exercise).as_str());
progress_bar.enable_steady_tick(100); progress_bar.enable_steady_tick(100);
@ -52,7 +60,12 @@ pub fn test(exercise: &Exercise) -> Result<(), ()> {
let formatstr = format!("{} Successfully tested {}!", Emoji("", ""), exercise); let formatstr = format!("{} Successfully tested {}!", Emoji("", ""), exercise);
println!("{}", style(formatstr).green()); println!("{}", style(formatstr).green());
exercise.clean(); exercise.clean();
Ok(()) if let State::Pending(context) = exercise.state() {
print_everything_looks_good(exercise.mode, context);
Ok(false)
} else {
Ok(true)
}
} else { } else {
let formatstr = format!( let formatstr = format!(
"{} Testing of {} failed! Please try again. Here's the output:", "{} Testing of {} failed! Please try again. Here's the output:",
@ -77,3 +90,34 @@ pub fn test(exercise: &Exercise) -> Result<(), ()> {
Err(()) 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
);
}
}

View File

View File

@ -0,0 +1,7 @@
// fake_exercise
// I AM NOT DONE
fn main() {
}

View File

@ -1,4 +1,7 @@
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
use glob::glob;
use std::fs::File;
use std::io::Read;
use std::process::Command; use std::process::Command;
#[test] #[test]
@ -105,3 +108,20 @@ fn run_single_test_no_exercise() {
.assert() .assert()
.code(1); .code(1);
} }
#[test]
fn all_exercises_require_confirmation() {
for exercise in glob("exercises/**/*.rs").unwrap() {
let path = exercise.unwrap();
let source = {
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
s
};
source.matches("// I AM NOT DONE").next().expect(&format!(
"There should be an `I AM NOT DONE` annotation in {:?}",
path
));
}
}