use std::path::PathBuf;
use serde::Deserialize;
use super::{artifacts::Artifact, stderr::StderrTail};
pub(super) struct Messages(Vec<Message>);
impl Messages {
pub(super) fn parse(stdout: &str) -> Self {
Self(
stdout
.lines()
.filter_map(|line| serde_json::from_str(line).ok())
.collect(),
)
}
pub(super) fn failure_diagnostics(&self, stderr: &StderrTail) -> String {
let diagnostics = if self.has_compiler_error() {
self.rendered_diagnostics()
} else {
stderr.error_output()
};
diagnostics.trim_end().to_string()
}
fn has_compiler_error(&self) -> bool {
self.diagnostics().any(Diagnostic::is_error)
}
fn rendered_diagnostics(&self) -> String {
self.diagnostics()
.filter_map(|diagnostic| diagnostic.rendered.as_deref())
.collect()
}
fn diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
self.0.iter().filter_map(|message| match message {
Message::Compiler { message } => Some(message),
_ => None,
})
}
pub(super) fn artifacts(&self) -> Vec<PathBuf> {
self.0
.iter()
.filter_map(|message| match message {
Message::Artifact(artifact) => Some(artifact),
_ => None,
})
.flat_map(Artifact::final_outputs)
.collect()
}
}
#[derive(Deserialize)]
#[serde(tag = "reason")]
enum Message {
#[serde(rename = "compiler-message")]
Compiler { message: Diagnostic },
#[serde(rename = "compiler-artifact")]
Artifact(Artifact),
#[serde(other)]
Other,
}
#[derive(Deserialize)]
struct Diagnostic {
level: String,
rendered: Option<String>,
}
impl Diagnostic {
fn is_error(&self) -> bool {
self.level.starts_with("error")
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn messages(values: &[serde_json::Value]) -> Messages {
let lines: Vec<String> = values.iter().map(ToString::to_string).collect();
Messages::parse(&lines.join("\n"))
}
fn stderr(text: &str) -> StderrTail {
let mut tail = StderrTail::new();
tail.push(text.as_bytes());
tail
}
fn diagnostic(level: &str, rendered: &str) -> serde_json::Value {
json!({
"reason": "compiler-message",
"message": { "level": level, "rendered": rendered },
})
}
#[test]
fn rustc_errors_are_reported_from_the_json_stream() {
let messages = messages(&[
diagnostic("warning", "warning: unused variable `x`\n"),
diagnostic("error", "error[E0308]: mismatched types\n"),
]);
assert_eq!(
messages.failure_diagnostics(&stderr(
" Compiling app v0.1.0\nerror: could not compile `app` (bin \"app\") due to 1 previous error\n"
)),
"warning: unused variable `x`\nerror[E0308]: mismatched types"
);
}
#[test]
fn build_script_failures_are_reported_from_cargo_stderr() {
let messages = messages(&[
json!({
"reason": "compiler-artifact",
"target": { "crate_types": ["bin"] },
"executable": null,
"filenames": ["/target/debug/build/app-abc/build-script-build"],
}),
json!({ "reason": "build-finished", "success": false }),
]);
let stderr = stderr(concat!(
" Compiling app v0.1.0 (/app)\n",
" Building [=> ] 0/3: app(build.rs) \r",
" Building [====> ] 1/3: app(build) \r",
"error: failed to run custom build command for `app v0.1.0 (/app)`\n",
"\n",
"Caused by:\n",
" process didn't exit successfully: `build-script-build` (exit status: 1)\n",
" --- stderr\n",
" something went wrong in the build script\n",
));
assert_eq!(
messages.failure_diagnostics(&stderr),
concat!(
"error: failed to run custom build command for `app v0.1.0 (/app)`\n",
"\n",
"Caused by:\n",
" process didn't exit successfully: `build-script-build` (exit status: 1)\n",
" --- stderr\n",
" something went wrong in the build script",
)
);
}
#[test]
fn manifest_errors_are_reported_with_an_empty_json_stream() {
let messages = Messages::parse("");
assert_eq!(
messages.failure_diagnostics(&stderr(
"error: unclosed table, expected `]`\n --> Cargo.toml:8:14\n"
)),
"error: unclosed table, expected `]`\n --> Cargo.toml:8:14"
);
}
#[test]
fn warnings_alone_do_not_stand_in_for_a_cargo_failure() {
let messages = messages(&[diagnostic("warning", "warning: unused import\n")]);
assert_eq!(
messages.failure_diagnostics(&stderr(
" Compiling dep v0.1.0\nerror: no bin target named `nope`\n"
)),
"error: no bin target named `nope`"
);
}
#[test]
fn executables_are_kept_and_noise_ignored() {
let messages = messages(&[
json!({ "reason": "build-script-executed" }),
json!({
"reason": "compiler-artifact",
"target": { "crate_types": ["bin"] },
"executable": "/target/debug/app",
"filenames": ["/target/debug/app"],
}),
]);
assert_eq!(
messages.artifacts(),
vec![PathBuf::from("/target/debug/app")]
);
}
#[test]
fn multiple_final_library_outputs_are_all_kept() {
let messages = messages(&[
json!({
"reason": "compiler-artifact",
"target": { "crate_types": ["cdylib"] },
"executable": null,
"filenames": ["/target/liba.dylib"],
}),
json!({
"reason": "compiler-artifact",
"target": { "crate_types": ["cdylib"] },
"executable": null,
"filenames": ["/target/libb.dylib"],
}),
]);
assert_eq!(messages.artifacts().len(), 2);
}
}