Skip to content

Commit

Permalink
Auto merge of #143 - cjpearce:fix-exercise-path-matching, r=komaeda
Browse files Browse the repository at this point in the history
Canonicalize paths to fix path matching

This PR should fix #126. The main solution to the issue was using `canonicalize()` on the paths we create for the exercises from `info.toml` and any user-specified paths, so that path `ends_with` matching will work correctly.

As adding calls to the canonicalize function everywhere requires unwrapping, I also decided to extract a struct representing an exercise and use serde to deserialize the paths from the `info.toml` file up front. I also tried to move the path handling out into the `exercise.rs` file and down into `main.rs` so that it doesn't create as much clutter. There was already a lot of unwrapping and path handling in the other files and I felt like it was getting a bit too repetitive.

If the approach is going too far (too many changes etc.) I'm happy to try to produce a smaller PR that fixes the bug without any refactoring.
  • Loading branch information
bors committed Apr 13, 2019
2 parents 04d1d4c + 77de6e5 commit 8387de6
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 128 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ console = "0.6.2"
syntect = "3.0.2"
notify = "4.0.0"
toml = "0.4.10"
serde = {version = "1.0.10", features = ["derive"]}

[[bin]]
name = "rustlings"
Expand Down
79 changes: 79 additions & 0 deletions src/exercise.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use serde::Deserialize;
use std::fmt::{self, Display, Formatter};
use std::fs::{remove_file};
use std::path::{PathBuf};
use std::process::{self, Command, Output};

const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];

fn temp_file() -> String {
format!("./temp_{}", process::id())
}

#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Compile,
Test,
}

#[derive(Deserialize)]
pub struct ExerciseList {
pub exercises: Vec<Exercise>,
}

#[derive(Deserialize)]
pub struct Exercise {
pub path: PathBuf,
pub mode: Mode,
}

impl Exercise {
pub fn compile(&self) -> Output {
match self.mode {
Mode::Compile => Command::new("rustc")
.args(&[self.path.to_str().unwrap(), "-o", &temp_file()])
.args(RUSTC_COLOR_ARGS)
.output(),
Mode::Test => Command::new("rustc")
.args(&["--test", self.path.to_str().unwrap(), "-o", &temp_file()])
.args(RUSTC_COLOR_ARGS)
.output(),
}
.expect("Failed to run 'compile' command.")
}

pub fn run(&self) -> Output {
Command::new(&temp_file())
.output()
.expect("Failed to run 'run' command")
}

pub fn clean(&self) {
let _ignored = remove_file(&temp_file());
}
}

impl Display for Exercise {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.path.to_str().unwrap())
}
}

#[cfg(test)]
mod test {
use super::*;
use std::path::Path;
use std::fs::File;

#[test]
fn test_clean() {
File::create(&temp_file()).unwrap();
let exercise = Exercise {
path: PathBuf::from("example.rs"),
mode: Mode::Test,
};
exercise.clean();
assert!(!Path::new(&temp_file()).exists());
}
}
42 changes: 34 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::exercise::{Exercise, ExerciseList};
use crate::run::run;
use crate::verify::verify;
use clap::{crate_version, App, Arg, SubCommand};
use notify::DebouncedEvent;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::ffi::OsStr;
use std::fs;
use std::io::BufRead;
use std::path::Path;
use std::sync::mpsc::channel;
Expand All @@ -13,8 +15,8 @@ use syntect::highlighting::{Style, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::as_24_bit_terminal_escaped;

mod exercise;
mod run;
mod util;
mod verify;

fn main() {
Expand Down Expand Up @@ -56,16 +58,36 @@ fn main() {
std::process::exit(1);
}

if let Some(matches) = matches.subcommand_matches("run") {
run(matches.clone()).unwrap_or_else(|_| std::process::exit(1));
let toml_str = &fs::read_to_string("info.toml").unwrap();
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;

if let Some(ref matches) = matches.subcommand_matches("run") {
let filename = matches.value_of("file").unwrap_or_else(|| {
println!("Please supply a file name!");
std::process::exit(1);
});

let matching_exercise = |e: &&Exercise| {
Path::new(filename)
.canonicalize()
.map(|p| p.ends_with(&e.path))
.unwrap_or(false)
};

let exercise = exercises.iter().find(matching_exercise).unwrap_or_else(|| {
println!("No exercise found for your file name!");
std::process::exit(1)
});

run(&exercise).unwrap_or_else(|_| std::process::exit(1));
}

if matches.subcommand_matches("verify").is_some() {
verify(None).unwrap_or_else(|_| std::process::exit(1));
verify(&exercises).unwrap_or_else(|_| std::process::exit(1));
}

if matches.subcommand_matches("watch").is_some() {
watch().unwrap();
watch(&exercises).unwrap();
}

if matches.subcommand_name().is_none() {
Expand All @@ -81,21 +103,25 @@ fn main() {
println!("\x1b[0m");
}

fn watch() -> notify::Result<()> {
fn watch(exercises: &[Exercise]) -> notify::Result<()> {
let (tx, rx) = channel();

let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?;
watcher.watch(Path::new("./exercises"), RecursiveMode::Recursive)?;

let _ignored = verify(None);
let _ignored = verify(exercises.iter());

loop {
match rx.recv() {
Ok(event) => match event {
DebouncedEvent::Create(b) | DebouncedEvent::Chmod(b) | DebouncedEvent::Write(b) => {
if b.extension() == Some(OsStr::new("rs")) {
println!("----------**********----------\n");
let _ignored = verify(Some(b.as_path().to_str().unwrap()));
let filepath = b.as_path().canonicalize().unwrap();
let exercise = exercises
.iter()
.skip_while(|e| !filepath.ends_with(&e.path));
let _ignored = verify(exercise);
}
}
_ => {}
Expand Down
51 changes: 17 additions & 34 deletions src/run.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,52 @@
use crate::util;
use crate::exercise::{Mode, Exercise};
use crate::verify::test;
use console::{style, Emoji};
use indicatif::ProgressBar;
use std::fs;
use toml::Value;

pub fn run(matches: clap::ArgMatches) -> Result<(), ()> {
if let Some(filename) = matches.value_of("file") {
let toml: Value = fs::read_to_string("info.toml").unwrap().parse().unwrap();
let tomlvec: &Vec<Value> = toml.get("exercises").unwrap().as_array().unwrap();
let mut exercises = tomlvec.clone();
exercises.retain(|i| i.get("path").unwrap().as_str().unwrap() == filename);
if exercises.is_empty() {
println!("No exercise found for your filename!");
std::process::exit(1);
}

let exercise: &Value = &exercises[0];
match exercise.get("mode").unwrap().as_str().unwrap() {
"test" => test(exercise.get("path").unwrap().as_str().unwrap())?,
"compile" => compile_and_run(exercise.get("path").unwrap().as_str().unwrap())?,
_ => (),
}
Ok(())
} else {
panic!("Please supply a filename!");
pub fn run(exercise: &Exercise) -> Result<(), ()> {
match exercise.mode {
Mode::Test => test(exercise)?,
Mode::Compile => compile_and_run(exercise)?,
}
Ok(())
}

pub fn compile_and_run(filename: &str) -> Result<(), ()> {
pub fn compile_and_run(exercise: &Exercise) -> Result<(), ()> {
let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {}...", filename).as_str());
progress_bar.set_message(format!("Compiling {}...", exercise).as_str());
progress_bar.enable_steady_tick(100);

let compilecmd = util::compile_cmd(filename);
progress_bar.set_message(format!("Running {}...", filename).as_str());
let compilecmd = exercise.compile();
progress_bar.set_message(format!("Running {}...", exercise).as_str());
if compilecmd.status.success() {
let runcmd = util::run_cmd();
let runcmd = exercise.run();
progress_bar.finish_and_clear();

if runcmd.status.success() {
println!("{}", String::from_utf8_lossy(&runcmd.stdout));
let formatstr = format!("{} Successfully ran {}", Emoji("✅", "✓"), filename);
let formatstr = format!("{} Successfully ran {}", Emoji("✅", "✓"), exercise);
println!("{}", style(formatstr).green());
util::clean();
exercise.clean();
Ok(())
} else {
println!("{}", String::from_utf8_lossy(&runcmd.stdout));
println!("{}", String::from_utf8_lossy(&runcmd.stderr));

let formatstr = format!("{} Ran {} with errors", Emoji("⚠️ ", "!"), filename);
let formatstr = format!("{} Ran {} with errors", Emoji("⚠️ ", "!"), exercise);
println!("{}", style(formatstr).red());
util::clean();
exercise.clean();
Err(())
}
} else {
progress_bar.finish_and_clear();
let formatstr = format!(
"{} Compilation of {} failed! Compiler error message:\n",
Emoji("⚠️ ", "!"),
filename
exercise
);
println!("{}", style(formatstr).red());
println!("{}", String::from_utf8_lossy(&compilecmd.stderr));
util::clean();
exercise.clean();
Err(())
}
}
41 changes: 0 additions & 41 deletions src/util.rs

This file was deleted.

Loading

0 comments on commit 8387de6

Please sign in to comment.