Skip to content

Commit

Permalink
parse more largely from stdout and not jus stderr (#140)
Browse files Browse the repository at this point in the history
Due to #124 especially, we're more likely to receive test
failure abstracts on stdout and not just stderr

Fix #317
  • Loading branch information
Canop authored Jun 19, 2023
1 parent 30f850c commit db482d1
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 116 deletions.
1 change: 1 addition & 0 deletions src/command_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
145 changes: 85 additions & 60 deletions src/line_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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)
Expand All @@ -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
///
Expand Down
112 changes: 56 additions & 56 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit db482d1

Please sign in to comment.