From 4d45699c5329db2ecb85ddcbbdf59829498901aa Mon Sep 17 00:00:00 2001 From: Canop Date: Sat, 17 Jun 2023 20:35:47 +0200 Subject: [PATCH] parse more largely from stdout and not jus stderr Due to #124 especially, we're more likely to receive test failure abstracts on stdout and not just stderr Fix #317 --- src/command_result.rs | 1 + src/line_analysis.rs | 145 +++++++++++++++++++++++++----------------- src/report.rs | 112 ++++++++++++++++---------------- src/tty.rs | 8 +++ 4 files changed, 150 insertions(+), 116 deletions(-) diff --git a/src/command_result.rs b/src/command_result.rs index c5d74d2..5573437 100644 --- a/src/command_result.rs +++ b/src/command_result.rs @@ -25,6 +25,7 @@ impl CommandResult { let lines = &output.lines; let error_code = exit_status.and_then(|s| s.code()).filter(|&c| c != 0); let mut report = Report::from_lines(lines)?; + debug!("report stats: {:?}", &report.stats); if let Some(error_code) = error_code { if report.stats.errors + report.stats.test_fails == 0 { // report shows no error while the command exe reported diff --git a/src/line_analysis.rs b/src/line_analysis.rs index caac0dd..c888132 100644 --- a/src/line_analysis.rs +++ b/src/line_analysis.rs @@ -19,68 +19,72 @@ impl From<&CommandOutputLine> for LineAnalysis { fn from(cmd_line: &CommandOutputLine) -> Self { let content = &cmd_line.content; let mut key = None; - let line_type = match cmd_line.origin { - CommandStream::StdOut => { - // there's currently a not understood bug preventing us from getting - // style information in stdout. - if cmd_line.content.is_blank() { - LineType::Normal - } else if let Some(content) = cmd_line.content.if_unstyled() { - if let Some((k, r)) = as_test_result(content) { - key = Some(k.to_string()); - LineType::TestResult(r) - } else if let Some(k) = as_fail_result_title(content) { - key = Some(k.to_string()); - LineType::Title(Kind::TestFail) - } else if regex_is_match!("^failures:$", content) { - // this isn't very discriminant... + let line_type = if cmd_line.content.is_blank() { + LineType::Normal + } else if let Some(content) = cmd_line.content.if_unstyled() { + if let Some((k, r)) = as_test_result(content) { + key = Some(k.to_string()); + LineType::TestResult(r) + } else if let Some(k) = as_fail_result_title(content) { + key = Some(k.to_string()); + LineType::Title(Kind::TestFail) + } else if regex_is_match!("^failures:$", content) { + // this isn't very discriminant... + LineType::Title(Kind::Sum) + } else if regex_is_match!("^note: run with `RUST_BACKTRACE=", content) { + LineType::BacktraceSuggestion + } else { + LineType::Normal + } + } else { + if let (Some(title), Some(body)) = (content.strings.get(0), content.strings.get(1)) { + match ( + title.csi.as_ref(), + title.raw.as_ref(), + body.csi.as_ref(), + body.raw.as_ref(), + ) { + (CSI_BOLD_RED, "error", CSI_ERROR_BODY, body_raw) + if body_raw.starts_with(": aborting due to") => + { LineType::Title(Kind::Sum) - } else if regex_is_match!("^note: run with `RUST_BACKTRACE=", content) { - LineType::BacktraceSuggestion - } else { - LineType::Normal } - } else { - warn!("unexpected styled stdout: {:#?}", &cmd_line); - LineType::Normal // unexpected styled content - } - } - CommandStream::StdErr => { - if let (Some(title), Some(body)) = (content.strings.get(0), content.strings.get(1)) - { - match ( - title.csi.as_ref(), - title.raw.as_ref(), - body.csi.as_ref(), - body.raw.as_ref(), - ) { - (CSI_BOLD_RED, "error", CSI_ERROR_BODY, body_raw) - if body_raw.starts_with(": aborting due to") => - { - LineType::Title(Kind::Sum) - } - (CSI_BOLD_RED, title_raw, CSI_ERROR_BODY, _) - if title_raw.starts_with("error") => - { - LineType::Title(Kind::Error) - } - #[cfg(not(windows))] - (CSI_BOLD_YELLOW, "warning", _, body_raw) => { - determine_warning_type(body_raw, content) - } - #[cfg(windows)] - (CSI_BOLD_YELLOW | CSI_BOLD_4BIT_YELLOW, "warning", _, body_raw) => { - determine_warning_type(body_raw, content) + (CSI_BOLD_RED, title_raw, CSI_ERROR_BODY, _) + if title_raw.starts_with("error") => + { + LineType::Title(Kind::Error) + } + #[cfg(not(windows))] + (CSI_BOLD_YELLOW, "warning", _, body_raw) => { + determine_warning_type(body_raw, content) + } + #[cfg(windows)] + (CSI_BOLD_YELLOW | CSI_BOLD_4BIT_YELLOW, "warning", _, body_raw) => { + determine_warning_type(body_raw, content) + } + ("", title_raw, CSI_BOLD_BLUE, "--> ") if is_spaces(title_raw) => { + LineType::Location + } + ("", k, CSI_BOLD_RED|CSI_RED, "FAILED") if content.strings.len() == 2 => { + if let Some(k) = as_test_name(k) { + key = Some(k.to_string()); + LineType::TestResult(false) + } else { + LineType::Normal } - ("", title_raw, CSI_BOLD_BLUE, "--> ") if is_spaces(title_raw) => { - debug!("LOCATION {:#?}", &content); - LineType::Location + } + ("", k, CSI_GREEN, "ok") => { + if let Some(k) = as_test_name(k) { + key = Some(k.to_string()); + LineType::TestResult(true) + } else { + LineType::Normal } - _ => LineType::Normal, } - } else { - LineType::Normal // empty line + _ => LineType::Normal, } + } else { + LineType::Normal // empty line } }; LineAnalysis { line_type, key } @@ -91,7 +95,11 @@ fn determine_warning_type( body_raw: &str, content: &TLine, ) -> LineType { - if is_n_warnings_emitted(body_raw) || is_generated_n_warnings(content.strings.get(2)) { + info!("DETER WT {:?}", &content); + if is_n_warnings_emitted(body_raw) + || is_generated_n_warnings(&content.strings) + || is_build_failed(content.strings.get(2)) + { LineType::Title(Kind::Sum) } else { LineType::Title(Kind::Warning) @@ -106,12 +114,29 @@ fn is_spaces(s: &str) -> bool { fn is_n_warnings_emitted(s: &str) -> bool { regex_is_match!(r#"^: \d+ warnings? emitted"#, s) } - -fn is_generated_n_warnings(ts: Option<&TString>) -> bool { - ts.map_or(false, |ts| { +fn is_generated_n_warnings(ts: &[TString]) -> bool { + ts.iter().any(|ts| { regex_is_match!(r#"generated \d+ warnings?$"#, &ts.raw) }) } +fn is_build_failed(ts: Option<&TString>) -> bool { + ts.map_or(false, |ts| { + regex_is_match!(r#"^\s*build failed"#, &ts.raw) + }) +} + + +/// similar to as_test_result but without the FAILED|ok part +/// This is used in case of styled output (because the FAILED|ok +/// part is in another TString) +fn as_test_name(s: &str) -> Option<&str> { + regex_captures!( + r#"^test\s+(.+?)(?: - should panic\s*)?(?: - compile\s*)?\s+...\s*$"#, + s + ) + .map(|(_, key)| key) +} + /// return Some when the line is the non detailled /// result of a test, for example /// diff --git a/src/report.rs b/src/report.rs index f8c9653..34296fc 100644 --- a/src/report.rs +++ b/src/report.rs @@ -53,79 +53,78 @@ impl Report { let mut is_in_out_fail = false; let mut suggest_backtrace = false; for cmd_line in cmd_lines { - debug!("cmd_line={:?}", &cmd_line); let line_analysis = LineAnalysis::from(cmd_line); - debug!("line_analysis={:?}", &line_analysis); let line_type = line_analysis.line_type; let mut line = Line { item_idx: 0, // will be filled later line_type, content: cmd_line.content.clone(), }; - match cmd_line.origin { - CommandStream::StdErr => { - match line_type { - LineType::Title(Kind::Sum) => { - // we're not interested in this section - cur_err_kind = None; - } - LineType::Title(kind) => { - cur_err_kind = Some(kind); + debug!("{:?}> [{line_type:?}][{:?}]", cmd_line.origin, line_analysis.key); + match (line_type, line_analysis.key) { + (LineType::TestResult(r), Some(key)) => { + if r { + passed_tests += 1; + } else { + // we should receive the test failure section later, + // right now we just whitelist it + failure_names.insert(key); + } + } + (LineType::Title(Kind::TestFail), Some(key)) => { + if failure_names.contains(&key) { + failure_names.remove(&key); + line.content = TLine::failed(&key); + fails.push(line); + is_in_out_fail = true; + cur_err_kind = Some(Kind::TestFail); + } else { + warn!( + "unexpected test result failure_names={:?}, key={:?}", + &failure_names, &key, + ); + } + } + (LineType::Normal, None) => { + if line.content.is_blank() && cur_err_kind != Some(Kind::TestFail) { + is_in_out_fail = false; + } + if is_in_out_fail { + fails.push(line); + } else { + match cur_err_kind { + Some(Kind::Warning) => warnings.push(line), + Some(Kind::Error) => errors.push(line), + _ => {} } - _ => {} } + } + (LineType::Title(Kind::Sum), None) => { + // we're not interested in this section + cur_err_kind = None; + is_in_out_fail = false; + } + (LineType::Title(kind), _) => { + cur_err_kind = Some(kind); match cur_err_kind { Some(Kind::Warning) => warnings.push(line), Some(Kind::Error) => errors.push(line), _ => {} // before warnings and errors, or in a sum } } - CommandStream::StdOut => { - match (line_type, line_analysis.key) { - (LineType::TestResult(r), Some(key)) => { - if r { - passed_tests += 1; - } else { - // we should receive the test failure section later, - // right now we just whitelist it - failure_names.insert(key); - } - } - (LineType::Title(Kind::TestFail), Some(key)) => { - if failure_names.contains(&key) { - failure_names.remove(&key); - line.content = TLine::failed(&key); - fails.push(line); - is_in_out_fail = true; - cur_err_kind = Some(Kind::TestFail); - } else { - warn!( - "unexpected test result failure_names={:?}, key={:?}", - &failure_names, &key, - ); - } - } - (LineType::Normal, None) => { - if line.content.is_blank() && cur_err_kind != Some(Kind::TestFail) { - is_in_out_fail = false; - } else if is_in_out_fail { - fails.push(line); - } - } - (LineType::Title(Kind::Sum), None) => { - // we're not interested in this section - cur_err_kind = None; - is_in_out_fail = false; - } - (LineType::BacktraceSuggestion, _) => { - suggest_backtrace = true; - } - _ => { - // TODO add normal if not broken with blank line - warn!("unexpected line: {:#?}", &line); - } + (LineType::BacktraceSuggestion, _) => { + suggest_backtrace = true; + } + (LineType::Location, _) => { + match cur_err_kind { + Some(Kind::Warning) => warnings.push(line), + Some(Kind::Error) => errors.push(line), + Some(Kind::TestFail) => fails.push(line), + _ => {} // before warnings and errors, or in a sum } + suggest_backtrace = true; } + _ => {} } } // for now, we only added the test failures for which there was an output. @@ -158,6 +157,7 @@ impl Report { // have been read but not added (at start or end) let mut stats = Stats::from(&lines); stats.passed_tests = passed_tests; + debug!("stats: {:#?}", &stats); Ok(Report { lines, stats, diff --git a/src/tty.rs b/src/tty.rs index 03f8657..c24cf5e 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -12,6 +12,9 @@ pub const CSI_RESET: &str = "\u{1b}[0m\u{1b}[0m"; pub const CSI_BOLD: &str = "\u{1b}[1m"; pub const CSI_ITALIC: &str = "\u{1b}[3m"; +pub const CSI_GREEN: &str = "\u{1b}[32m"; + +pub const CSI_RED: &str = "\u{1b}[31m"; pub const CSI_BOLD_RED: &str = "\u{1b}[1m\u{1b}[38;5;9m"; pub const CSI_BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m"; @@ -247,6 +250,11 @@ impl TLine { None } } + pub fn has(&self, part: &str) -> bool { + self.strings + .iter() + .any(|s| s.raw.contains(part)) + } } #[derive(Debug, Default)]