summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfmoko <mokou@posteo.de>2021-01-17 12:37:58 +0100
committerGitHub <noreply@github.com>2021-01-17 12:37:58 +0100
commite8d1baa4b50faf038bec7629b559a0735173ee80 (patch)
tree2041ef8a6c7beea2a2f156c564a1e0b65620171e
parent0d65753fdb41be12310bec970123911aaee76235 (diff)
parent8bbe4ff1385c5c169c90cd3ff9253f9a91daaf8e (diff)
Merge pull request #599 from AbdouSeck/improve-list-command
feat(cli): Improve the list command with options, and then some
-rw-r--r--src/exercise.rs10
-rw-r--r--src/main.rs132
-rw-r--r--tests/fixture/state/info.toml7
-rw-r--r--tests/integration_tests.rs78
4 files changed, 207 insertions, 20 deletions
diff --git a/src/exercise.rs b/src/exercise.rs
index 283b2b9..7afa230 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -232,6 +232,16 @@ path = "{}.rs""#,
State::Pending(context)
}
+
+ // Check that the exercise looks to be solved using self.state()
+ // This is not the best way to check since
+ // the user can just remove the "I AM NOT DONE" string fromm the file
+ // without actually having solved anything.
+ // The only other way to truly check this would to compile and run
+ // the exercise; which would be both costly and counterintuitive
+ pub fn looks_done(&self) -> bool {
+ self.state() == State::Done
+ }
}
impl Display for Exercise {
diff --git a/src/main.rs b/src/main.rs
index bf35e4f..75a9cec 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,7 +7,7 @@ use notify::DebouncedEvent;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::ffi::OsStr;
use std::fs;
-use std::io;
+use std::io::{self, prelude::*};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::channel;
@@ -58,6 +58,45 @@ fn main() {
SubCommand::with_name("list")
.alias("l")
.about("Lists the exercises available in rustlings")
+ .arg(
+ Arg::with_name("paths")
+ .long("paths")
+ .short("p")
+ .conflicts_with("names")
+ .help("Show only the paths of the exercises")
+ )
+ .arg(
+ Arg::with_name("names")
+ .long("names")
+ .short("n")
+ .conflicts_with("paths")
+ .help("Show only the names of the exercises")
+ )
+ .arg(
+ Arg::with_name("filter")
+ .long("filter")
+ .short("f")
+ .takes_value(true)
+ .empty_values(false)
+ .help(
+ "Provide a string to match the exercise names.\
+ \nComma separated patterns are acceptable."
+ )
+ )
+ .arg(
+ Arg::with_name("unsolved")
+ .long("unsolved")
+ .short("u")
+ .conflicts_with("solved")
+ .help("Display only exercises not yet solved")
+ )
+ .arg(
+ Arg::with_name("solved")
+ .long("solved")
+ .short("s")
+ .conflicts_with("unsolved")
+ .help("Display only exercises that have been solved")
+ )
)
.get_matches();
@@ -93,9 +132,51 @@ fn main() {
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
let verbose = matches.is_present("nocapture");
- if matches.subcommand_matches("list").is_some() {
- exercises.iter().for_each(|e| println!("{}", e.name));
+ // Handle the list command
+ if let Some(list_m) = matches.subcommand_matches("list") {
+ if ["paths", "names"].iter().all(|k| !list_m.is_present(k)) {
+ println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
+ }
+ let filters = list_m.value_of("filter").unwrap_or_default().to_lowercase();
+ exercises.iter().for_each(|e| {
+ let fname = format!("{}", e.path.display());
+ let filter_cond = filters
+ .split(',')
+ .filter(|f| f.trim().len() > 0)
+ .any(|f| e.name.contains(&f) || fname.contains(&f));
+ let status = if e.looks_done() { "Done" } else { "Pending" };
+ let solve_cond = {
+ (e.looks_done() && list_m.is_present("solved"))
+ || (!e.looks_done() && list_m.is_present("unsolved"))
+ || (!list_m.is_present("solved") && !list_m.is_present("unsolved"))
+ };
+ if solve_cond && (filter_cond || !list_m.is_present("filter")) {
+ let line = if list_m.is_present("paths") {
+ format!("{}\n", fname)
+ } else if list_m.is_present("names") {
+ format!("{}\n", e.name)
+ } else {
+ format!("{:<17}\t{:<46}\t{:<7}\n", e.name, fname, status)
+ };
+ // Somehow using println! leads to the binary panicking
+ // when its output is piped.
+ // So, we're handling a Broken Pipe error and exiting with 0 anyway
+ let stdout = std::io::stdout();
+ {
+ let mut handle = stdout.lock();
+ handle.write_all(line.as_bytes()).unwrap_or_else(|e| {
+ match e.kind() {
+ std::io::ErrorKind::BrokenPipe => std::process::exit(0),
+ _ => std::process::exit(1),
+ };
+ });
+ }
+ }
+ });
+ std::process::exit(0);
}
+
+ // Handle the run command
if let Some(ref matches) = matches.subcommand_matches("run") {
let name = matches.value_of("name").unwrap();
@@ -123,13 +204,18 @@ fn main() {
println!("{}", exercise.hint);
}
+ // Handle the verify command
if matches.subcommand_matches("verify").is_some() {
verify(&exercises, verbose).unwrap_or_else(|_| std::process::exit(1));
}
+ // Handle the watch command
if matches.subcommand_matches("watch").is_some() {
if let Err(e) = watch(&exercises, verbose) {
- println!("Error: Could not watch your progess. Error message was {:?}.", e);
+ println!(
+ "Error: Could not watch your progess. Error message was {:?}.",
+ e
+ );
println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
std::process::exit(1);
}
@@ -138,24 +224,24 @@ fn main() {
emoji = Emoji("🎉", "★")
);
println!();
- println!("+----------------------------------------------------+");
- println!("| You made it to the Fe-nish line! |");
- println!("+-------------------------- ------------------------+");
+ println!("+----------------------------------------------------+");
+ println!("| You made it to the Fe-nish line! |");
+ println!("+-------------------------- ------------------------+");
println!(" \\/ ");
- println!(" â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’ ");
- println!(" â–’â–’â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’â–’â–’ ");
- println!(" â–’â–’â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’â–’â–’ ");
- println!(" â–‘â–‘â–’â–’â–’â–’â–‘â–‘â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’â–‘â–‘â–’â–’â–’â–’ ");
- println!(" ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ ");
- println!(" ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ ");
- println!(" â–’â–’ â–’â–’â–’â–’â–’â–’ â–’â–’â–’â–’â–’â–’ â–’â–’â–’â–’â–’â–’ â–’â–’ ");
- println!(" â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–“â–“â–“â–“â–“â–“â–’â–’â–’â–’â–’â–’â–’â–’â–“â–“â–’â–’â–“â–“â–’â–’â–’â–’â–’â–’â–’â–’ ");
+ println!(" â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’ ");
+ println!(" â–’â–’â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’â–’â–’ ");
+ println!(" â–’â–’â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’â–’â–’ ");
+ println!(" â–‘â–‘â–’â–’â–’â–’â–‘â–‘â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’â–‘â–‘â–’â–’â–’â–’ ");
+ println!(" ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ ");
+ println!(" ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ ");
+ println!(" â–’â–’ â–’â–’â–’â–’â–’â–’ â–’â–’â–’â–’â–’â–’ â–’â–’â–’â–’â–’â–’ â–’â–’ ");
+ println!(" â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–“â–“â–“â–“â–“â–“â–’â–’â–’â–’â–’â–’â–’â–’â–“â–“â–’â–’â–“â–“â–’â–’â–’â–’â–’â–’â–’â–’ ");
println!(" â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’ ");
println!(" ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ ");
- println!(" ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ ");
- println!(" â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’ ");
- println!(" â–’â–’ â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’ â–’â–’ ");
- println!(" â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ ");
+ println!(" ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ ");
+ println!(" â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’ ");
+ println!(" â–’â–’ â–’â–’ â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’ â–’â–’ â–’â–’ ");
+ println!(" â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ â–’â–’ ");
println!(" â–’â–’ â–’â–’ â–’â–’ â–’â–’ ");
println!();
println!("We hope you enjoyed learning about the various aspects of Rust!");
@@ -223,7 +309,13 @@ fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
let filepath = b.as_path().canonicalize().unwrap();
let pending_exercises = exercises
.iter()
- .skip_while(|e| !filepath.ends_with(&e.path));
+ .skip_while(|e| !filepath.ends_with(&e.path))
+ // .filter(|e| filepath.ends_with(&e.path))
+ .chain(
+ exercises
+ .iter()
+ .filter(|e| !e.looks_done() && !filepath.ends_with(&e.path))
+ );
clear_screen();
match verify(pending_exercises, verbose) {
Ok(_) => return Ok(()),
diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml
index 7bfc697..547b3a4 100644
--- a/tests/fixture/state/info.toml
+++ b/tests/fixture/state/info.toml
@@ -9,3 +9,10 @@ name = "pending_test_exercise"
path = "pending_test_exercise.rs"
mode = "test"
hint = """"""
+
+[[exercises]]
+name = "finished_exercise"
+path = "finished_exercise.rs"
+mode = "compile"
+hint = """"""
+
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 2baf9b8..f5211b6 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -181,3 +181,81 @@ fn run_single_test_success_without_output() {
.code(0)
.stdout(predicates::str::contains("THIS TEST TOO SHALL PAS").not());
}
+
+#[test]
+fn run_rustlings_list() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list"])
+ .current_dir("tests/fixture/success")
+ .assert()
+ .success();
+}
+
+#[test]
+fn run_rustlings_list_conflicting_display_options() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list", "--names", "--paths"])
+ .current_dir("tests/fixture/success")
+ .assert()
+ .failure();
+}
+
+#[test]
+fn run_rustlings_list_conflicting_solve_options() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list", "--solved", "--unsolved"])
+ .current_dir("tests/fixture/success")
+ .assert()
+ .failure();
+}
+
+#[test]
+fn run_rustlings_list_no_pending() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list"])
+ .current_dir("tests/fixture/success")
+ .assert()
+ .success()
+ .stdout(predicates::str::contains("Pending").not());
+}
+
+#[test]
+fn run_rustlings_list_both_done_and_pending() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list"])
+ .current_dir("tests/fixture/state")
+ .assert()
+ .success()
+ .stdout(
+ predicates::str::contains("Done")
+ .and(predicates::str::contains("Pending"))
+ );
+}
+
+#[test]
+fn run_rustlings_list_without_pending() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list", "--solved"])
+ .current_dir("tests/fixture/state")
+ .assert()
+ .success()
+ .stdout(predicates::str::contains("Pending").not());
+}
+
+#[test]
+fn run_rustlings_list_without_done() {
+ Command::cargo_bin("rustlings")
+ .unwrap()
+ .args(&["list", "--unsolved"])
+ .current_dir("tests/fixture/state")
+ .assert()
+ .success()
+ .stdout(predicates::str::contains("Done").not());
+}
+