Repository: cristianoliveira/funzzy (language: rust)

Issue:
bug: cli panic when scaping chars 
Try this:
```yaml
- name: run tests
  run: ["yarn test {{filepath}}", "echo '{{filepath}}' | sed -r 's\/.tsx/\/'" ]
  change: 'src/**'
```

And once the cli attempts to load the rules it panics:
```
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ScanError { mark: Marker { index: 258, line: 8, col: 34 }, info: "while parsing a quoted scalar, found unknown escape character" }', src/rules.rs:92:57
stack backtrace:
   0: rust_begin_unwind
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library/std/src/panicking.rs:593:5
   1: core::panicking::panic_fmt
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library/core/src/panicking.rs:67:14
   2: core::result::unwrap_failed
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library/core/src/result.rs:1651:5
   3: fzz::rules::from_file
   4: fzz::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```



Discussion / hints:

Fix patch:
diff --git a/src/main.rs b/src/main.rs
index b04efc8..19c3a37 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -94,13 +94,18 @@ fn main() {
                         show("The list of files received is empty");
                     }
 
+                    let watch_rules = match rules::from_string(content, arg_command) {
+                        Ok(rules) => rules,
+                        Err(err) => error("Failed to get rules from stdin", err),
+                    };
+
                     execute_watch_command(
-                        Watches::new(rules::from_string(content, arg_command)),
+                        Watches::new(watch_rules),
                         args.flag_n,
                         args.flag_V,
                     );
                 }
-                Err(err) => error("Error while greading stdin", err),
+                Err(err) => error("Failed to read stdin", err),
             };
         }
 
@@ -148,7 +153,7 @@ fn main() {
                     }
                 }
 
-                Err(err) => error("Error while reading config file", err),
+                Err(err) => error("Failed to read config file", err),
             }
         }
     }
@@ -175,11 +180,11 @@ fn from_stdin() -> Result<String, String> {
     let has_input = std::sync::Arc::new(std::sync::Mutex::new(false));
     let clone_has_input = has_input.clone();
     std::thread::spawn(move || {
-        std::thread::sleep(std::time::Duration::from_secs(5));
+        std::thread::sleep(std::time::Duration::from_secs(1));
 
         let had_input = *clone_has_input.lock().expect("Could not lock has_input");
         if !had_input {
-            stdout::info("Timed out waiting for input after 5 seconds");
+            stdout::info("Timed out waiting for input after 1 seconds");
             std::process::exit(0);
         }
     });
@@ -187,7 +192,13 @@ fn from_stdin() -> Result<String, String> {
     let mut buffer = String::new();
     match stdin.lock().read_to_string(&mut buffer) {
         Ok(bytes) => {
-            let mut has_input_mutex = has_input.lock().unwrap();
+            let mut has_input_mutex = match has_input.lock() {
+                Ok(mutex) => mutex,
+                Err(err) =>  {
+                    return Err(format!("Could not lock stdin mutex {}", err));
+                }
+            };
+
             *has_input_mutex = bytes > 0;
             if bytes > 0 {
                 Ok(buffer)
@@ -213,6 +224,8 @@ fn show(text: &str) -> ! {
 }
 
 fn error(text: &str, err: String) -> ! {
-    println!("{} cause: {}", text, err);
+    println!("Execution failed");
+    println!("  {}", text);
+    println!("  {}", err);
     std::process::exit(1);
 }
diff --git a/src/rules.rs b/src/rules.rs
index d5ad5b4..834df4b 100644
--- a/src/rules.rs
+++ b/src/rules.rs
@@ -89,15 +89,31 @@ pub fn template(commands: Vec<String>, filepath: &str) -> Vec<String> {
 }
 
 pub fn from_yaml(file_content: &str) -> Result<Vec<Rules>, String> {
-    let items = YamlLoader::load_from_str(file_content).unwrap();
+    let items = match YamlLoader::load_from_str(file_content) {
+        Ok(val) => val,
+        Err(err) => {
+            return Err(format!(
+                "Found an invalid yaml format {}",
+                err
+            ));
+        }
+    };
     match items[0] {
         Yaml::Array(ref items) => Ok(items.iter().map(Rules::from).collect()),
         _ => Err("You must have at last one item in the yaml.".to_owned()),
     }
 }
 
-pub fn from_string(patterns: String, command: String) -> Vec<Rules> {
-    let current_dir = std::env::current_dir().expect("Cannot get current directory");
+pub fn from_string(patterns: String, command: String) -> Result<Vec<Rules>, String> {
+    let current_dir = match std::env::current_dir() {
+        Ok(val) => val,
+        Err(err) => {
+            return Err(format!(
+                "Failed to get current directory {}",
+                err
+            ));
+        }
+    };
 
     let watches = patterns
         .lines()
@@ -115,42 +131,53 @@ pub fn from_string(patterns: String, command: String) -> Vec<Rules> {
             };
 
             if full_path.is_dir() {
-                return full_path
-                    .join("**")
-                    .to_str()
-                    .unwrap_or_else(|| panic!("Cannot convert {:?} to path with wildcard", line))
-                    .to_owned();
+                let full_path_as_str = match full_path.join("**").to_str() {
+                    Some(val) => val.to_owned(),
+                    None => {
+                        println!("Warning: Was not possible to convert {} to absolute path", line);
+
+                        String::from("")
+                    }
+                };
+
+                return full_path_as_str;
             }
 
-            full_path
-                .to_str()
-                .unwrap_or_else(|| panic!("Cannot convert {:?} to absolute path", line))
-                .to_owned()
+            match full_path.to_str() {
+                Some(val) => val.to_owned(),
+                None => {
+                    println!("Warning: Was not possible to convert {} to absolute path", line);
+
+                    String::from("")
+                }
+            }
         })
         .collect::<Vec<String>>();
 
     stdout::info(&format!("watching patterns \n {}", watches.join("\n ")));
 
-    vec![Rules::new(
+    Ok(vec![Rules::new(
         "unnamed".to_owned(),
         vec![command],
         watches,
         vec![],
         false,
-    )]
+    )])
 }
 
 pub fn from_file(filename: &str) -> Result<Vec<Rules>, String> {
     match File::open(filename) {
         Ok(mut file) => {
             let mut content = String::new();
-            file.read_to_string(&mut content)
-                .unwrap_or_else(|_| panic!("Cannot read file {}", filename));
 
-            match from_yaml(&content) {
-                Err(err) => Err(err),
-                rules => rules,
+            if let Err(err) = file.read_to_string(&mut content) {
+                return Err(format!(
+                    "Invalid config file format '{}'. Details: {}",
+                    filename, err
+                ));
             }
+
+            return from_yaml(&content);
         }
 
         Err(err) => Err(format!(
@@ -327,7 +354,7 @@ mod tests {
     #[test]
     fn it_does_not_filters_empty_or_one_character_path() {
         let content = "./foo\n./bar\n.\n./baz\n";
-        let rules = from_string(String::from(content), String::from("cargo test"));
+        let rules = from_string(String::from(content), String::from("cargo test")).unwrap();
         assert!(rules[0].watch(&get_absolute_path("foo")));
         assert!(rules[0].watch(&get_absolute_path("bar")));
         assert!(rules[0].watch(&get_absolute_path("baz")));
diff --git a/src/watches.rs b/src/watches.rs
index c15a8c6..e5c6a4c 100644
--- a/src/watches.rs
+++ b/src/watches.rs
@@ -67,7 +67,7 @@ mod tests {
     #[test]
     fn it_loads_from_args() {
         let args = String::from("cargo build");
-        let watches = Watches::new(rules::from_string(".".to_owned(), args));
+        let watches = Watches::new(rules::from_string(".".to_owned(), args).unwrap());
 
         assert!(watches.watch(&get_absolute_path("src/main.rs")).is_some());
         assert!(watches.watch(&get_absolute_path("test/main.rs")).is_some());
@@ -235,4 +235,38 @@ mod tests {
             ]
         );
     }
+
+    #[test]
+    fn it_returns_an_error_when_fail_to_load_config_file() {
+        assert!(rules::from_yaml(
+            &r#"
+        - name: run tests
+          run: [
+            "yarn test {{filepath}}", 
+            "echo '{{filepath}}' | sed -r 's\/.tsx/\/'" 
+          ]
+          change: 'src/**'
+        "#
+        )
+        .is_err());
+
+        assert!(rules::from_yaml(
+            &r#"
+        - name: run tests
+          run: [
+            "yarn test {{filepath}}", 
+          change: 'src/**'
+        "#
+        )
+        .is_err());
+
+        assert!(rules::from_yaml(
+            &r#"
+        - name: other
+          run: 'cargo test'
+          change: 'test/**'
+        "#
+        )
+        .is_ok());
+    }
 }
diff --git a/src/yaml.rs b/src/yaml.rs
index 166cd58..ac5ea85 100644
--- a/src/yaml.rs
+++ b/src/yaml.rs
@@ -6,11 +6,14 @@ pub fn extract_strings(yaml: &Yaml) -> Vec<String> {
     match yaml.clone() {
         Yaml::Array(ref items) => items
             .iter()
-            .map(|i| String::from(i.as_str().unwrap()))
+            .map(|i| String::from(i.as_str().unwrap_or_else(|| "_invalid_value_")))
             .collect(),
         Yaml::String(ref item) => vec![String::from(item.as_str())],
         Yaml::BadValue => vec![],
-        _ => panic!("Unkown format. Please review the yaml"),
+        unknown => {
+            println!("Error: config file has an unknown type. {:?}", unknown);
+            panic!("Interrupted due the previou error.")
+        }
     }
 }
 
@@ -25,7 +28,7 @@ pub fn extract_bool(yaml: &Yaml) -> bool {
 
 pub fn validate(yaml: &Yaml, key: &str) {
     if yaml[key].is_badvalue() {
-        println!("File has a bad format. (Key {} not found)", key);
-        panic!("Panicate due the previou error.")
+        println!("Config file has an invalid format. (Key {} was not found)", key);
+        panic!("Interrupted due the previou error.")
     }
 }


Test patch: