Repository: Canop/bacon (language: rust)

Issue:
[Feature request] Ignore some messages by regex
I uses `cargo:warning` to inform some messages in `build.rs`.
This is because `cargo:info` is not supported yet.  ( https://github.com/rust-lang/cargo/issues/7037 )

bacon handles these messages as warning, but it can't be resolved.
So I think message filtering by regex may be useful.

For example, an actual message is like below:

```
warning: veryl-std@0.13.0: std hash: 158094aee3b99767fbd3cf75a1be02bf31a4aa5215dc39053c4e81a3b6617b8b
```

I expect the above message is ignored by a configuration like below

```
ignore_messages = ["warning: veryl-std@.*"]
```


Discussion / hints:
Can you please build a minimal project demonstrating the problem ?
Please refer https://github.com/dalance/demo_for_bacon

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc67320..9b794d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 - support for nextest output & default nextest job, bound by default to the 'n' key - Fix #196
 - new `exports` structure in configuration. New `analysis` export bound by default to `ctrl-e`. The old syntax defining locations export is still supported but won't appear in documentations anymore.
 - recognize panic location in test - Fix #208
+- lines to ignore can be specified as a set of regular expressions in a `ignored_lines` field either in the job or at the top of the prefs or bacon.toml - Fix #223
 - `toggle-backtrace` accepts an optional level: `toggle-backtrace(1)` or `toggle-backtrace(full)` - Experimental - Fix #210
 
 <a name="v2.21.0"></a>
diff --git a/src/analysis/analyzer.rs b/src/analysis/analyzer.rs
index 782f4d4..d0cfdd4 100644
--- a/src/analysis/analyzer.rs
+++ b/src/analysis/analyzer.rs
@@ -32,6 +32,7 @@ impl Analyzer {
     pub fn build_report(
         &self,
         cmd_lines: &[CommandOutputLine],
+        mission: &Mission,
     ) -> anyhow::Result<Report> {
         #[derive(Debug, Default)]
         struct Failure {
@@ -46,7 +47,20 @@ impl Analyzer {
         let mut cur_err_kind = None; // the current kind among stderr lines
         let mut is_in_out_fail = false;
         let mut suggest_backtrace = false;
+        let ignore_patterns = mission
+            .job
+            .ignored_lines
+            .as_ref()
+            .or(mission.settings.ignored_lines.as_ref())
+            .filter(|p| !p.is_empty());
         for cmd_line in cmd_lines {
+            if let Some(patterns) = ignore_patterns {
+                let raw_line = cmd_line.content.to_raw();
+                if patterns.iter().any(|p| p.raw_line_is_match(&raw_line)) {
+                    debug!("ignoring line: {}", &raw_line);
+                    continue;
+                }
+            }
             let line_analysis = self.analyze_line(cmd_line);
             let line_type = line_analysis.line_type;
             let mut line = Line {
diff --git a/src/app.rs b/src/app.rs
index eba329e..99ca0b0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -121,7 +121,7 @@ pub fn run(
                             info!("execution finished with status: {:?}", status);
                             // computation finished
                             let output = state.take_output().unwrap_or_default();
-                            let cmd_result = CommandResult::build(output, status, analyzer)?;
+                            let cmd_result = CommandResult::build(output, status, analyzer, &state.mission)?;
                             state.set_result(cmd_result);
                             action = state.action();
                         }
diff --git a/src/command_result.rs b/src/command_result.rs
index 21d7fae..c0e6d4a 100644
--- a/src/command_result.rs
+++ b/src/command_result.rs
@@ -24,10 +24,11 @@ impl CommandResult {
         output: CommandOutput,
         exit_status: Option<ExitStatus>,
         analyzer: Analyzer,
+        mission: &Mission,
     ) -> Result<Self> {
         let lines = &output.lines;
         let error_code = exit_status.and_then(|s| s.code()).filter(|&c| c != 0);
-        let mut report = analyzer.build_report(lines)?;
+        let mut report = analyzer.build_report(lines, mission)?;
         debug!("report stats: {:?}", &report.stats);
         if let Some(error_code) = error_code {
             if report.stats.errors + report.stats.test_fails == 0 {
diff --git a/src/config.rs b/src/config.rs
index 80857c1..b00dfaa 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -50,6 +50,10 @@ pub struct Config {
     pub vim_keys: Option<bool>,
 
     pub wrap: Option<bool>,
+
+    /// Patterns of lines which should be ignored. Patterns of
+    /// the prefs or bacon.toml can be overridden at the job
+    pub ignored_lines: Option<Vec<LinePattern>>,
 }
 
 impl Config {
diff --git a/src/job.rs b/src/job.rs
index c25e20e..ffe4b3d 100644
--- a/src/job.rs
+++ b/src/job.rs
@@ -87,6 +87,10 @@ pub struct Job {
     /// The analyzer interpreting the output of the command, the
     /// standard cargo dedicated one if not provided
     pub analyzer: Option<Analyzer>,
+
+    /// Patterns of lines which should be ignored. Patterns of
+    /// the prefs or bacon.toml can be overridden at the job
+    pub ignored_lines: Option<Vec<LinePattern>>,
 }
 
 static DEFAULT_ARGS: &[&str] = &["--color", "always"];
@@ -128,6 +132,7 @@ impl Job {
             extraneous_args: true,
             on_change_strategy: None,
             analyzer: Some(Analyzer::Standard),
+            ignored_lines: None,
         }
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 233e75b..ac34a38 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -23,6 +23,7 @@ mod job_stack;
 mod keybindings;
 mod line;
 mod line_analysis;
+mod line_pattern;
 mod line_type;
 mod list_jobs;
 mod mission;
@@ -63,6 +64,7 @@ pub use {
     keybindings::*,
     line::*,
     line_analysis::*,
+    line_pattern::*,
     line_type::*,
     list_jobs::*,
     mission::*,
diff --git a/src/line_pattern.rs b/src/line_pattern.rs
new file mode 100644
index 0000000..0373efc
--- /dev/null
+++ b/src/line_pattern.rs
@@ -0,0 +1,44 @@
+use {
+    lazy_regex::regex::Regex,
+    serde::{
+        Deserialize,
+        Deserializer,
+        de,
+    },
+    std::str::FromStr,
+};
+
+/// A pattern dedicated to line matching.
+///
+/// In the future, this may become more complex (eg filtering by style or origin)
+#[derive(Debug, Clone)]
+pub struct LinePattern {
+    pub regex: Regex,
+}
+
+impl LinePattern {
+    pub fn raw_line_is_match(
+        &self,
+        line: &str,
+    ) -> bool {
+        self.regex.is_match(line)
+    }
+}
+
+impl FromStr for LinePattern {
+    type Err = String;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let regex = Regex::new(s).map_err(|e| format!("invalid regex: {}", e))?;
+        Ok(Self { regex })
+    }
+}
+
+impl<'de> Deserialize<'de> for LinePattern {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        FromStr::from_str(&s).map_err(de::Error::custom)
+    }
+}
diff --git a/src/settings.rs b/src/settings.rs
index ce74286..7636d8b 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -30,6 +30,7 @@ pub struct Settings {
     pub exports: ExportsSettings,
     pub show_changes_count: bool,
     pub on_change_strategy: Option<OnChangeStrategy>,
+    pub ignored_lines: Option<Vec<LinePattern>>,
 }
 
 impl Default for Settings {
@@ -51,6 +52,7 @@ impl Default for Settings {
             exports: Default::default(),
             show_changes_count: false,
             on_change_strategy: None,
+            ignored_lines: Default::default(),
         }
     }
 }
@@ -98,6 +100,9 @@ impl Settings {
         if let Some(b) = config.on_change_strategy {
             self.on_change_strategy = Some(b);
         }
+        if let Some(b) = config.ignored_lines.as_ref() {
+            self.ignored_lines = Some(b.clone());
+        }
     }
     pub fn apply_args(
         &mut self,
diff --git a/website/docs/config.md b/website/docs/config.md
index 503a5a7..7d3d845 100644
--- a/website/docs/config.md
+++ b/website/docs/config.md
@@ -101,6 +101,7 @@ command | the tokens making the command to execute (first one is the executable)
 default_watch | whether to watch default files (`src`, `tests`, `examples`, `build.rs`, and `benches`). When it's set to `false`, only the files in your `watch` parameter are watched | `true`
 env | a map of environment vars, for example `env.LOG_LEVEL="die"` |
 kill | a command replacing the default job interruption (platform dependant, `SIGKILL` on unix). For example `kill = ["kill", "-s", "INT"]` |
+ignored_lines | regular expressions for lines to ignores. If set before all jobs, will apply to all of them | none
 extraneous_args | if `false`, the action is run "as is" from `bacon.toml`, eg: no `--all-features` or `--features` inclusion | `true`
 need_stdout |whether we need to capture stdout too (stderr is always captured) | `false`
 on_change_strategy | `wait_then_restart` or `kill_then_restart` |


Test patch:
diff --git a/src/analysis/nextest_analyzer.rs b/src/analysis/nextest_analyzer.rs
index 3510aef..396ea36 100644
--- a/src/analysis/nextest_analyzer.rs
+++ b/src/analysis/nextest_analyzer.rs
@@ -92,9 +92,7 @@ fn is_canceling(content: &TLine) -> bool {
 /// In the future, we might want to return the duration too.
 fn as_test_result(content: &TLine) -> Option<(String, bool)> {
     let mut strings = content.strings.iter();
-    let Some(first) = strings.next() else {
-        return None;
-    };
+    let first = strings.next()?;
     let pass = match (first.csi.as_str(), first.raw.trim()) {
         (CSI_PASS, "PASS") => true,
         (CSI_ERROR, "FAIL") => false,