crate_compile_test/steps/
check_errors.rs1use failure::ResultExt;
2use regex::Regex;
3use serde_json as json;
4use std::cmp;
5use std::fmt;
6use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use walkdir::WalkDir;
11
12use super::{TestStep, TestStepFactory};
13use cargo_messages;
14use config::{Config, Profile};
15use error::{Result, TestingError};
16
17pub use cargo_messages::DiagnosticLevel;
18
19#[derive(Debug, Clone, PartialEq, Deserialize)]
20pub struct MessageLocation {
21 pub file: PathBuf,
22 pub line: usize,
23}
24
25#[derive(Debug, Clone)]
26pub enum MessageType {
27 None,
28 Text(String),
29 Regex(Regex),
30}
31
32#[derive(Debug, Clone)]
33pub struct CompilerMessage {
34 pub message: MessageType,
35 pub level: DiagnosticLevel,
36 pub code: Option<String>,
37 pub location: Option<MessageLocation>,
38}
39
40pub struct CheckErrorsStepFactory;
41
42struct CheckErrorsStep {
43 crate_dir: PathBuf,
44 expected_messages: Vec<CompilerMessage>,
45}
46
47impl CheckErrorsStepFactory {
48 pub fn new() -> Self {
49 CheckErrorsStepFactory {}
50 }
51
52 pub fn collect_crate_messages(crate_path: &Path) -> Result<Vec<CompilerMessage>> {
53 let sources = WalkDir::new(&crate_path.join("src"))
54 .into_iter()
55 .map(|entry| entry.unwrap())
56 .filter_map(
57 |entry| match entry.path().extension().and_then(|item| item.to_str()) {
58 Some("rs") => Some(PathBuf::from(entry.path())),
59 _ => None,
60 },
61 );
62
63 let mut messages = vec![];
64
65 for path in sources {
66 let source_path = path.strip_prefix(crate_path)?;
67 let source_file = BufReader::new({
68 File::open(&path).context(format!("Unable to open source at {:?}", path))?
69 });
70
71 source_file.lines().fold(1, |line_num, line| {
72 Self::analyse_source_line(&source_path, (line_num, &line.unwrap()), &mut messages);
73
74 line_num + 1
75 });
76 }
77
78 Ok(messages)
79 }
80
81 fn analyse_source_line(path: &Path, line: (usize, &str), messages: &mut Vec<CompilerMessage>) {
82 lazy_static! {
83 static ref ERR_CODE_REGEX: Regex = Regex::new(r"^ *E\d{4} *$").unwrap();
84 static ref MESSAGE_REGEX: Regex =
85 Regex::new(r"// *~([\^]+|[\|])? +(ERROR|WARNING|NOTE|HELP) +(.+)").unwrap();
86 static ref GLOBAL_MESSAGE_REGEX: Regex =
87 Regex::new(r"// *~ +GLOBAL-(ERROR|WARNING|NOTE|HELP)-REGEX +(.+)").unwrap();
88 }
89
90 if let Some(captures) = GLOBAL_MESSAGE_REGEX.captures(line.1) {
91 let message = CompilerMessage {
92 message: MessageType::Regex(Regex::new(captures[2].trim()).unwrap()),
93
94 code: None,
95 location: None,
96 level: captures[1].into(),
97 };
98
99 messages.push(message);
100 }
101
102 if let Some(captures) = MESSAGE_REGEX.captures(line.1) {
103 let location = match captures.get(1).map(|item| item.as_str()) {
104 Some("|") => messages
105 .iter()
106 .last()
107 .and_then(|item| item.location.clone()),
108
109 None => Some(MessageLocation {
110 file: path.into(),
111 line: line.0,
112 }),
113
114 relative @ _ => Some(MessageLocation {
115 file: path.into(),
116 line: line.0 - relative.unwrap().len(),
117 }),
118 };
119
120 let (message, code) = match ERR_CODE_REGEX.is_match(&captures[3]) {
121 true => (None, Some(captures[3].trim().into())),
122 false => (Some(captures[3].trim().into()), None),
123 };
124
125 let message = CompilerMessage {
126 message: message
127 .map(|item| MessageType::Text(item))
128 .unwrap_or(MessageType::None),
129
130 code,
131 location,
132 level: captures[2].into(),
133 };
134
135 messages.push(message);
136 }
137 }
138}
139
140impl CheckErrorsStep {
141 pub fn new(crate_dir: PathBuf, expected_messages: Vec<CompilerMessage>) -> Self {
142 CheckErrorsStep {
143 crate_dir,
144 expected_messages,
145 }
146 }
147
148 fn find_actual_messages(&self, config: &Config, path: &Path) -> Result<Vec<CompilerMessage>> {
149 let mut command = Command::new(&config.cargo_command);
150
151 command.current_dir(&self.crate_dir);
152 command.env("CARGO_TARGET_DIR", path);
153 command.args(&["build", "--message-format", "json"]);
154
155 if let Some(target) = config.target.as_ref() {
156 command.args(&["--target", target]);
157 }
158
159 if config.profile == Profile::Release {
160 command.arg("--release");
161 }
162
163 for (key, value) in &config.cargo_env {
164 command.env(key, value);
165 }
166
167 let mut actual_messages = vec![];
168
169 let raw_output = command.output()?;
170 let stderr = String::from_utf8_lossy(&raw_output.stderr).into_owned();
171 let stdout = String::from_utf8_lossy(&raw_output.stdout).into_owned();
172
173 for line in stdout.lines() {
174 let message = {
175 json::from_str::<cargo_messages::Diagnostic>(line)
176 .context("Unable to parse Cargo JSON output")?
177 };
178
179 match (message.reason.as_str(), message.message) {
180 ("compiler-message", Some(message)) => {
181 if message.spans.len() == 0 {
182 for child in &message.children {
183 actual_messages.push(child.clone().into());
184 }
185 }
186
187 if !message.message.starts_with("aborting")
188 && message.level != DiagnosticLevel::Empty
189 {
190 actual_messages.push(message.into());
191 }
192 }
193
194 _ => {}
195 };
196 }
197
198 match raw_output.status.success() {
199 false => {
200 if actual_messages.len() > 0 {
201 Ok(actual_messages)
202 } else {
203 bail!(TestingError::CrateBuildFailed { stdout, stderr })
204 }
205 }
206
207 true => bail!(TestingError::UnexpectedBuildSuccess),
208 }
209 }
210}
211
212impl TestStepFactory for CheckErrorsStepFactory {
213 fn initialize(&self, _config: &Config, crate_path: &Path) -> Result<Box<TestStep>> {
214 Ok(Box::new(CheckErrorsStep::new(
215 crate_path.into(),
216 Self::collect_crate_messages(crate_path)?,
217 )))
218 }
219}
220
221impl TestStep for CheckErrorsStep {
222 fn execute(&self, config: &Config, build_path: &Path) -> Result<()> {
223 let actual_messages = self.find_actual_messages(config, build_path)?;
224
225 let unexpected_messages: Vec<_> = actual_messages
226 .clone()
227 .into_iter()
228 .filter(|item| !self.expected_messages.contains(item))
229 .collect();
230
231 let missing_messages: Vec<_> = self.expected_messages
232 .clone()
233 .into_iter()
234 .filter(|item| !actual_messages.contains(item))
235 .collect();
236
237 if unexpected_messages.len() > 0 || missing_messages.len() > 0 {
238 bail!(TestingError::MessageExpectationsFailed {
239 unexpected: unexpected_messages,
240 missing: missing_messages,
241 });
242 }
243
244 Ok(())
245 }
246}
247
248impl cmp::PartialEq for CompilerMessage {
249 fn eq(&self, other: &CompilerMessage) -> bool {
250 if self.location != other.location || self.level != other.level {
251 return false;
252 }
253
254 if self.code.is_some() && other.code.is_some() {
255 return self.code.as_ref().unwrap() == other.code.as_ref().unwrap();
256 }
257
258 match (&self.message, &other.message) {
259 (MessageType::Text(ref lhs), MessageType::Text(ref rhs)) => lhs == rhs,
260
261 (MessageType::Text(ref lhs), MessageType::Regex(ref rhs)) => rhs.is_match(lhs),
262 (MessageType::Regex(ref lhs), MessageType::Text(ref rhs)) => lhs.is_match(rhs),
263
264 (MessageType::Regex(ref lhs), MessageType::Regex(ref rhs)) => {
265 lhs.as_str() == rhs.as_str()
266 }
267
268 _ => false,
269 }
270 }
271}
272
273impl fmt::Display for CompilerMessage {
274 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
275 match self.location {
276 Some(ref location) => {
277 writeln!(
278 f,
279 "file: {}:{}",
280 &location.file.to_string_lossy(),
281 location.line
282 )?;
283 }
284
285 None => {
286 writeln!(f, "file: none",)?;
287 }
288 };
289
290 f.write_str("message: ")?;
291 f.write_str(&match self.code {
292 Some(ref code) => format!("({:?} {}) ", self.level, code),
293 None => format!("({:?}) ", self.level),
294 })?;
295
296 match self.message {
297 MessageType::Text(ref message) => write!(f, "{}", message)?,
298 MessageType::Regex(ref expr) => write!(f, "Regex({})", expr.as_str())?,
299
300 _ => {}
301 }
302
303 Ok(())
304 }
305}