Skip to content

Commit 8387de6

Browse files
committed
Auto merge of #143 - cjpearce:fix-exercise-path-matching, r=komaeda
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.
2 parents 04d1d4c + 77de6e5 commit 8387de6

File tree

6 files changed

+161
-128
lines changed

6 files changed

+161
-128
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ console = "0.6.2"
1111
syntect = "3.0.2"
1212
notify = "4.0.0"
1313
toml = "0.4.10"
14+
serde = {version = "1.0.10", features = ["derive"]}
1415

1516
[[bin]]
1617
name = "rustlings"

src/exercise.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use serde::Deserialize;
2+
use std::fmt::{self, Display, Formatter};
3+
use std::fs::{remove_file};
4+
use std::path::{PathBuf};
5+
use std::process::{self, Command, Output};
6+
7+
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
8+
9+
fn temp_file() -> String {
10+
format!("./temp_{}", process::id())
11+
}
12+
13+
#[derive(Deserialize)]
14+
#[serde(rename_all = "lowercase")]
15+
pub enum Mode {
16+
Compile,
17+
Test,
18+
}
19+
20+
#[derive(Deserialize)]
21+
pub struct ExerciseList {
22+
pub exercises: Vec<Exercise>,
23+
}
24+
25+
#[derive(Deserialize)]
26+
pub struct Exercise {
27+
pub path: PathBuf,
28+
pub mode: Mode,
29+
}
30+
31+
impl Exercise {
32+
pub fn compile(&self) -> Output {
33+
match self.mode {
34+
Mode::Compile => Command::new("rustc")
35+
.args(&[self.path.to_str().unwrap(), "-o", &temp_file()])
36+
.args(RUSTC_COLOR_ARGS)
37+
.output(),
38+
Mode::Test => Command::new("rustc")
39+
.args(&["--test", self.path.to_str().unwrap(), "-o", &temp_file()])
40+
.args(RUSTC_COLOR_ARGS)
41+
.output(),
42+
}
43+
.expect("Failed to run 'compile' command.")
44+
}
45+
46+
pub fn run(&self) -> Output {
47+
Command::new(&temp_file())
48+
.output()
49+
.expect("Failed to run 'run' command")
50+
}
51+
52+
pub fn clean(&self) {
53+
let _ignored = remove_file(&temp_file());
54+
}
55+
}
56+
57+
impl Display for Exercise {
58+
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
59+
write!(f, "{}", self.path.to_str().unwrap())
60+
}
61+
}
62+
63+
#[cfg(test)]
64+
mod test {
65+
use super::*;
66+
use std::path::Path;
67+
use std::fs::File;
68+
69+
#[test]
70+
fn test_clean() {
71+
File::create(&temp_file()).unwrap();
72+
let exercise = Exercise {
73+
path: PathBuf::from("example.rs"),
74+
mode: Mode::Test,
75+
};
76+
exercise.clean();
77+
assert!(!Path::new(&temp_file()).exists());
78+
}
79+
}

src/main.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
use crate::exercise::{Exercise, ExerciseList};
12
use crate::run::run;
23
use crate::verify::verify;
34
use clap::{crate_version, App, Arg, SubCommand};
45
use notify::DebouncedEvent;
56
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
67
use std::ffi::OsStr;
8+
use std::fs;
79
use std::io::BufRead;
810
use std::path::Path;
911
use std::sync::mpsc::channel;
@@ -13,8 +15,8 @@ use syntect::highlighting::{Style, ThemeSet};
1315
use syntect::parsing::SyntaxSet;
1416
use syntect::util::as_24_bit_terminal_escaped;
1517

18+
mod exercise;
1619
mod run;
17-
mod util;
1820
mod verify;
1921

2022
fn main() {
@@ -56,16 +58,36 @@ fn main() {
5658
std::process::exit(1);
5759
}
5860

59-
if let Some(matches) = matches.subcommand_matches("run") {
60-
run(matches.clone()).unwrap_or_else(|_| std::process::exit(1));
61+
let toml_str = &fs::read_to_string("info.toml").unwrap();
62+
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
63+
64+
if let Some(ref matches) = matches.subcommand_matches("run") {
65+
let filename = matches.value_of("file").unwrap_or_else(|| {
66+
println!("Please supply a file name!");
67+
std::process::exit(1);
68+
});
69+
70+
let matching_exercise = |e: &&Exercise| {
71+
Path::new(filename)
72+
.canonicalize()
73+
.map(|p| p.ends_with(&e.path))
74+
.unwrap_or(false)
75+
};
76+
77+
let exercise = exercises.iter().find(matching_exercise).unwrap_or_else(|| {
78+
println!("No exercise found for your file name!");
79+
std::process::exit(1)
80+
});
81+
82+
run(&exercise).unwrap_or_else(|_| std::process::exit(1));
6183
}
6284

6385
if matches.subcommand_matches("verify").is_some() {
64-
verify(None).unwrap_or_else(|_| std::process::exit(1));
86+
verify(&exercises).unwrap_or_else(|_| std::process::exit(1));
6587
}
6688

6789
if matches.subcommand_matches("watch").is_some() {
68-
watch().unwrap();
90+
watch(&exercises).unwrap();
6991
}
7092

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

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

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

90-
let _ignored = verify(None);
112+
let _ignored = verify(exercises.iter());
91113

92114
loop {
93115
match rx.recv() {
94116
Ok(event) => match event {
95117
DebouncedEvent::Create(b) | DebouncedEvent::Chmod(b) | DebouncedEvent::Write(b) => {
96118
if b.extension() == Some(OsStr::new("rs")) {
97119
println!("----------**********----------\n");
98-
let _ignored = verify(Some(b.as_path().to_str().unwrap()));
120+
let filepath = b.as_path().canonicalize().unwrap();
121+
let exercise = exercises
122+
.iter()
123+
.skip_while(|e| !filepath.ends_with(&e.path));
124+
let _ignored = verify(exercise);
99125
}
100126
}
101127
_ => {}

src/run.rs

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,52 @@
1-
use crate::util;
1+
use crate::exercise::{Mode, Exercise};
22
use crate::verify::test;
33
use console::{style, Emoji};
44
use indicatif::ProgressBar;
5-
use std::fs;
6-
use toml::Value;
75

8-
pub fn run(matches: clap::ArgMatches) -> Result<(), ()> {
9-
if let Some(filename) = matches.value_of("file") {
10-
let toml: Value = fs::read_to_string("info.toml").unwrap().parse().unwrap();
11-
let tomlvec: &Vec<Value> = toml.get("exercises").unwrap().as_array().unwrap();
12-
let mut exercises = tomlvec.clone();
13-
exercises.retain(|i| i.get("path").unwrap().as_str().unwrap() == filename);
14-
if exercises.is_empty() {
15-
println!("No exercise found for your filename!");
16-
std::process::exit(1);
17-
}
18-
19-
let exercise: &Value = &exercises[0];
20-
match exercise.get("mode").unwrap().as_str().unwrap() {
21-
"test" => test(exercise.get("path").unwrap().as_str().unwrap())?,
22-
"compile" => compile_and_run(exercise.get("path").unwrap().as_str().unwrap())?,
23-
_ => (),
24-
}
25-
Ok(())
26-
} else {
27-
panic!("Please supply a filename!");
6+
pub fn run(exercise: &Exercise) -> Result<(), ()> {
7+
match exercise.mode {
8+
Mode::Test => test(exercise)?,
9+
Mode::Compile => compile_and_run(exercise)?,
2810
}
11+
Ok(())
2912
}
3013

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

36-
let compilecmd = util::compile_cmd(filename);
37-
progress_bar.set_message(format!("Running {}...", filename).as_str());
19+
let compilecmd = exercise.compile();
20+
progress_bar.set_message(format!("Running {}...", exercise).as_str());
3821
if compilecmd.status.success() {
39-
let runcmd = util::run_cmd();
22+
let runcmd = exercise.run();
4023
progress_bar.finish_and_clear();
4124

4225
if runcmd.status.success() {
4326
println!("{}", String::from_utf8_lossy(&runcmd.stdout));
44-
let formatstr = format!("{} Successfully ran {}", Emoji("✅", "✓"), filename);
27+
let formatstr = format!("{} Successfully ran {}", Emoji("✅", "✓"), exercise);
4528
println!("{}", style(formatstr).green());
46-
util::clean();
29+
exercise.clean();
4730
Ok(())
4831
} else {
4932
println!("{}", String::from_utf8_lossy(&runcmd.stdout));
5033
println!("{}", String::from_utf8_lossy(&runcmd.stderr));
5134

52-
let formatstr = format!("{} Ran {} with errors", Emoji("⚠️ ", "!"), filename);
35+
let formatstr = format!("{} Ran {} with errors", Emoji("⚠️ ", "!"), exercise);
5336
println!("{}", style(formatstr).red());
54-
util::clean();
37+
exercise.clean();
5538
Err(())
5639
}
5740
} else {
5841
progress_bar.finish_and_clear();
5942
let formatstr = format!(
6043
"{} Compilation of {} failed! Compiler error message:\n",
6144
Emoji("⚠️ ", "!"),
62-
filename
45+
exercise
6346
);
6447
println!("{}", style(formatstr).red());
6548
println!("{}", String::from_utf8_lossy(&compilecmd.stderr));
66-
util::clean();
49+
exercise.clean();
6750
Err(())
6851
}
6952
}

src/util.rs

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)