crate_compile_test/steps/
check_errors.rs

1use 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}