1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
use {
crate::*,
anyhow::Result,
lazy_regex::*,
std::{
collections::HashSet,
io,
path::PathBuf,
},
};
/// the usable content of cargo watch's output,
/// lightly analyzed
#[derive(Debug)]
pub struct Report {
pub lines: Vec<Line>,
pub stats: Stats,
pub suggest_backtrace: bool,
pub output: CommandOutput,
}
impl Report {
/// change the order of the lines so that items are in reverse order
/// (but keep the order of lines of a given item)
pub fn reverse(&mut self) {
self.lines
.sort_by_key(|line| std::cmp::Reverse(line.item_idx));
}
/// A successful report is one with nothing to tell: no warning,
/// no error, no test failure
pub fn is_success(
&self,
allow_warnings: bool,
allow_failures: bool,
) -> bool {
!(self.stats.errors != 0
|| (!allow_failures && self.stats.test_fails != 0)
|| (!allow_warnings && self.stats.warnings != 0))
}
/// compute the report from the lines of stdout and/or stderr of the
/// `cargo` command.
///
/// We assume errors and warnings come in the stderr stream while
/// test failures come in stdout
pub fn from_lines(cmd_lines: &[CommandOutputLine]) -> Result<Report> {
// we first accumulate warnings, test fails and errors in separate vectors
let mut warnings = Vec::new();
let mut errors = Vec::new();
let mut fails = Vec::new();
let mut failure_names = HashSet::new();
let mut passed_tests = 0;
let mut cur_err_kind = None; // the current kind among stderr lines
let mut is_in_out_fail = false;
let mut suggest_backtrace = false;
for cmd_line in cmd_lines {
let line_analysis = LineAnalysis::from(cmd_line);
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(),
};
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
}
}
(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.
// We add the other ones
for key in failure_names.drain() {
fails.push(Line {
item_idx: 0, // will be filled later
line_type: LineType::Title(Kind::TestFail),
content: TLine::failed(&key),
});
fails.push(Line {
item_idx: 0,
line_type: LineType::Normal,
content: TLine::italic("no output".to_string()),
});
}
// we now build a common vector, with errors first
let mut lines = errors;
lines.append(&mut fails);
lines.append(&mut warnings);
// and we assign the indexes
let mut item_idx = 0;
for line in &mut lines {
if matches!(line.line_type, LineType::Title(_)) {
item_idx += 1;
}
line.item_idx = item_idx;
}
// we compute the stats at end because some lines may
// 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,
suggest_backtrace,
output: CommandOutput::default(),
})
}
/// export the report in a file
pub fn write_to<W: io::Write>(
&self,
w: &mut W,
mission: &Mission,
) -> Result<(), io::Error> {
let mut last_kind = "???";
let mut message = None;
for line in &self.lines {
match line.line_type {
LineType::Title(Kind::Warning) => {
last_kind = "warning";
message = line.title_message();
}
LineType::Title(Kind::Error) => {
last_kind = "error";
message = line.title_message();
}
LineType::Title(Kind::TestFail) => {
last_kind = "test";
message = line.title_message();
}
_ => {}
}
let Some(location) = line.location() else {
continue;
};
let (_, mut path, file_line, file_column) =
regex_captures!(r#"^([^:]+):(\d+):(\d+)$"#, location,)
.unwrap_or(("", location, "", ""));
// we need to make sure the path is absolute
let path_buf = PathBuf::from(path);
let path_string;
if path_buf.is_relative() {
path_string = mission
.workspace_root
.join(path)
.to_string_lossy()
.to_string();
path = &path_string;
}
let exported = regex_replace_all!(
r#"\{([^\s}]+)\}"#,
&mission.settings.export.line_format,
|_, key| {
match key {
"kind" => last_kind,
"path" => path,
"line" => file_line,
"column" => file_column,
"message" => message.unwrap_or(""),
_ => {
debug!("unknown export key: {key:?}");
""
}
}
}
);
writeln!(w, "{}", exported)?;
}
debug!("exported locations");
Ok(())
}
}