Skip to content

Commit d968d9b

Browse files
authored
Store failure reason into assess metadata (rust-lang#2166)
We now store Kani's version and the failure reason (if there's one) into the assess metadata. For now, we still need to manually parse this information, but this moves us closer to rust-lang#2058 and rust-lang#2165. In order to collect more details about the compilation errors, we now inspect the cargo diagnostic messages.
1 parent 9cb5005 commit d968d9b

File tree

11 files changed

+183
-40
lines changed

11 files changed

+183
-40
lines changed

kani-driver/src/assess/args.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub enum AssessSubcommand {
2828
/// `cargo kani assess scan` subcommand arguments
2929
#[derive(Debug, Parser)]
3030
pub struct ScanArgs {
31+
// TODO: When assess scan is invoked using `--existing-only`, it should check if the Kani version
32+
// from the existing metadata files matches the current version. Otherwise, the results may no
33+
// longer hold.
3134
/// Don't run assess on found packages, just re-analyze the results from a previous run
3235
#[arg(long, hide = true)]
3336
pub existing_only: bool,

kani-driver/src/assess/metadata.rs

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ use super::AssessArgs;
2727
/// This is not a stable interface.
2828
#[derive(Serialize, Deserialize)]
2929
pub struct AssessMetadata {
30+
/// Kani version that was used to generate the metadata.
31+
#[serde(rename = "kani_version")]
32+
pub version: String,
33+
/// Contains an error message in cases where it failed the execution.
34+
pub error: Option<SessionError>,
3035
/// Report on the presence of `codegen_unimplemented` in the analyzed packages
3136
pub unsupported_features: TableBuilder<UnsupportedFeaturesTableRow>,
3237
/// Report of the reasons why tests could not be analyzed by Kani
@@ -35,32 +40,62 @@ pub struct AssessMetadata {
3540
pub promising_tests: TableBuilder<PromisingTestsTableRow>,
3641
}
3742

43+
impl AssessMetadata {
44+
pub fn new(
45+
unsupported_features: TableBuilder<UnsupportedFeaturesTableRow>,
46+
failure_reasons: TableBuilder<FailureReasonsTableRow>,
47+
promising_tests: TableBuilder<PromisingTestsTableRow>,
48+
) -> AssessMetadata {
49+
AssessMetadata {
50+
version: env!("CARGO_PKG_VERSION").to_string(),
51+
error: None,
52+
unsupported_features,
53+
failure_reasons,
54+
promising_tests,
55+
}
56+
}
57+
58+
pub fn from_error(err: &dyn std::error::Error) -> AssessMetadata {
59+
let error = Some(SessionError {
60+
root_cause: err.source().map(|e| format!("{e:#}")),
61+
msg: err.to_string(),
62+
});
63+
AssessMetadata {
64+
version: env!("CARGO_PKG_VERSION").to_string(),
65+
error,
66+
unsupported_features: TableBuilder::new(),
67+
failure_reasons: TableBuilder::new(),
68+
promising_tests: TableBuilder::new(),
69+
}
70+
}
71+
pub fn empty() -> AssessMetadata {
72+
AssessMetadata {
73+
version: env!("CARGO_PKG_VERSION").to_string(),
74+
error: None,
75+
unsupported_features: TableBuilder::new(),
76+
failure_reasons: TableBuilder::new(),
77+
promising_tests: TableBuilder::new(),
78+
}
79+
}
80+
}
81+
82+
#[derive(Serialize, Deserialize, Debug)]
83+
pub struct SessionError {
84+
pub root_cause: Option<String>,
85+
pub msg: String,
86+
}
87+
3888
/// If given the argument to so do, write the assess metadata to the target file.
39-
pub(crate) fn write_metadata(args: &AssessArgs, build: AssessMetadata) -> Result<()> {
89+
pub(crate) fn write_metadata(args: &AssessArgs, metadata: AssessMetadata) -> Result<()> {
4090
if let Some(path) = &args.emit_metadata {
4191
let out_file = File::create(&path)?;
4292
let writer = BufWriter::new(out_file);
4393
// use pretty for now to keep things readable and debuggable, but this should change eventually
44-
serde_json::to_writer_pretty(writer, &build)?;
94+
serde_json::to_writer_pretty(writer, &metadata)?;
4595
}
4696
Ok(())
4797
}
4898

49-
/// Write metadata with unsupported features only, supporting the `--only-codegen` option.
50-
pub(crate) fn write_partial_metadata(
51-
args: &AssessArgs,
52-
unsupported_features: TableBuilder<UnsupportedFeaturesTableRow>,
53-
) -> Result<()> {
54-
write_metadata(
55-
args,
56-
AssessMetadata {
57-
unsupported_features,
58-
failure_reasons: TableBuilder::new(),
59-
promising_tests: TableBuilder::new(),
60-
},
61-
)
62-
}
63-
6499
/// Read assess metadata from a file.
65100
pub(crate) fn read_metadata(path: &Path) -> Result<AssessMetadata> {
66101
// this function already exists, but a proxy here helps find it :)
@@ -72,11 +107,7 @@ pub(crate) fn read_metadata(path: &Path) -> Result<AssessMetadata> {
72107
/// This is not a complicated operation, because the assess metadata structure is meant
73108
/// to accomodate multiple packages already, so we're just "putting it together".
74109
pub(crate) fn aggregate_metadata(metas: Vec<AssessMetadata>) -> AssessMetadata {
75-
let mut result = AssessMetadata {
76-
unsupported_features: TableBuilder::new(),
77-
failure_reasons: TableBuilder::new(),
78-
promising_tests: TableBuilder::new(),
79-
};
110+
let mut result = AssessMetadata::empty();
80111
for meta in metas {
81112
for item in meta.unsupported_features.build() {
82113
result.unsupported_features.add(item.clone());

kani-driver/src/assess/mod.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4+
use self::metadata::{write_metadata, AssessMetadata};
45
use anyhow::Result;
56
use kani_metadata::KaniMetadata;
67

8+
use crate::assess::table_builder::TableBuilder;
79
use crate::metadata::merge_kani_metadata;
810
use crate::project;
911
use crate::session::KaniSession;
@@ -21,11 +23,23 @@ mod table_unsupported_features;
2123
/// `cargo kani assess` main entry point.
2224
///
2325
/// See <https://model-checking.github.io/kani/dev-assess.html>
24-
pub(crate) fn cargokani_assess_main(mut session: KaniSession, args: AssessArgs) -> Result<()> {
26+
pub(crate) fn run_assess(session: KaniSession, args: AssessArgs) -> Result<()> {
2527
if let Some(args::AssessSubcommand::Scan(args)) = &args.command {
2628
return scan::assess_scan_main(session, args);
2729
}
2830

31+
let result = assess_project(session);
32+
match result {
33+
Ok(metadata) => write_metadata(&args, metadata),
34+
Err(err) => {
35+
let metadata = AssessMetadata::from_error(err.as_ref());
36+
write_metadata(&args, metadata)?;
37+
Err(err.context("Failed to assess project"))
38+
}
39+
}
40+
}
41+
42+
fn assess_project(mut session: KaniSession) -> Result<AssessMetadata> {
2943
// Fix (as in "make unchanging/unchangable") some settings.
3044
// This is a temporary hack to make things work, until we get around to refactoring how arguments
3145
// work generally in kani-driver. These arguments, for instance, are all prepended to the subcommand,
@@ -69,8 +83,11 @@ pub(crate) fn cargokani_assess_main(mut session: KaniSession, args: AssessArgs)
6983
}
7084

7185
if session.args.only_codegen {
72-
metadata::write_partial_metadata(&args, unsupported_features)?;
73-
return Ok(());
86+
return Ok(AssessMetadata::new(
87+
unsupported_features,
88+
TableBuilder::new(),
89+
TableBuilder::new(),
90+
));
7491
}
7592

7693
// Done with the 'cargo-kani' part, now we're going to run *test* harnesses instead of proof:
@@ -95,12 +112,7 @@ pub(crate) fn cargokani_assess_main(mut session: KaniSession, args: AssessArgs)
95112
let promising_tests = table_promising_tests::build(&results);
96113
println!("{}", promising_tests.render());
97114

98-
metadata::write_metadata(
99-
&args,
100-
metadata::AssessMetadata { unsupported_features, failure_reasons, promising_tests },
101-
)?;
102-
103-
Ok(())
115+
Ok(AssessMetadata::new(unsupported_features, failure_reasons, promising_tests))
104116
}
105117

106118
/// Merges a collection of Kani metadata by figuring out which package each belongs to, from cargo metadata.

kani-driver/src/call_cargo.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
use crate::args::KaniArgs;
55
use crate::session::{KaniSession, ReachabilityMode};
66
use anyhow::{bail, Context, Result};
7-
use cargo_metadata::{Metadata, MetadataCommand, Package};
7+
use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel};
8+
use cargo_metadata::{Message, Metadata, MetadataCommand, Package};
89
use std::ffi::OsString;
910
use std::fs;
11+
use std::io::BufReader;
1012
use std::path::{Path, PathBuf};
1113
use std::process::Command;
1214
use tracing::{debug, trace};
@@ -81,6 +83,10 @@ impl KaniSession {
8183
cargo_args.push("--target-dir".into());
8284
cargo_args.push(target_dir.into());
8385

86+
// Configuration needed to parse cargo compilation status.
87+
cargo_args.push("--message-format".into());
88+
cargo_args.push("json-diagnostic-rendered-ansi".into());
89+
8490
if self.args.tests {
8591
// Use test profile in order to pull dev-dependencies and compile using `--test`.
8692
// Initially the plan was to use `--tests` but that brings in multiple targets.
@@ -120,9 +126,10 @@ impl KaniSession {
120126
.args(&pkg_args)
121127
.env("RUSTC", &self.kani_compiler)
122128
.env("RUSTFLAGS", "--kani-flags")
123-
.env("KANIFLAGS", &crate::util::join_osstring(&kani_args, " "));
129+
.env("KANIFLAGS", &crate::util::join_osstring(&kani_args, " "))
130+
.env("CARGO_TERM_PROGRESS_WHEN", "never");
124131

125-
self.run_terminal(cmd)?;
132+
self.run_cargo(cmd)?;
126133
found_target = true;
127134
}
128135
}
@@ -167,6 +174,67 @@ impl KaniSession {
167174

168175
cmd.exec().context("Failed to get cargo metadata.")
169176
}
177+
178+
/// Run cargo and collect any error found.
179+
/// TODO: We should also use this to collect the artifacts generated by cargo.
180+
fn run_cargo(&self, cargo_cmd: Command) -> Result<()> {
181+
let support_color = atty::is(atty::Stream::Stdout);
182+
if let Some(mut cargo_process) = self.run_piped(cargo_cmd)? {
183+
let reader = BufReader::new(cargo_process.stdout.take().unwrap());
184+
let mut error_count = 0;
185+
for message in Message::parse_stream(reader) {
186+
let message = message.unwrap();
187+
match message {
188+
Message::CompilerMessage(msg) => match msg.message.level {
189+
DiagnosticLevel::FailureNote => {
190+
print_msg(&msg.message, support_color)?;
191+
}
192+
DiagnosticLevel::Error => {
193+
error_count += 1;
194+
print_msg(&msg.message, support_color)?;
195+
}
196+
DiagnosticLevel::Ice => {
197+
print_msg(&msg.message, support_color)?;
198+
let _ = cargo_process.wait();
199+
return Err(anyhow::Error::msg(msg.message).context(format!(
200+
"Failed to compile `{}` due to an internal compiler error.",
201+
msg.target.name
202+
)));
203+
}
204+
_ => {
205+
if !self.args.quiet {
206+
print_msg(&msg.message, support_color)?;
207+
}
208+
}
209+
},
210+
Message::CompilerArtifact(_)
211+
| Message::BuildScriptExecuted(_)
212+
| Message::BuildFinished(_) => {
213+
// do nothing
214+
}
215+
// Non-exhaustive enum.
216+
_ => {}
217+
}
218+
}
219+
let status = cargo_process.wait()?;
220+
if !status.success() {
221+
bail!(
222+
"Failed to execute cargo ({status}). Found {error_count} compilation errors."
223+
);
224+
}
225+
}
226+
Ok(())
227+
}
228+
}
229+
230+
/// Print the compiler message following the coloring schema.
231+
fn print_msg(diagnostic: &Diagnostic, use_rendered: bool) -> Result<()> {
232+
if use_rendered {
233+
print!("{diagnostic}");
234+
} else {
235+
print!("{}", console::strip_ansi_codes(diagnostic.rendered.as_ref().unwrap()));
236+
}
237+
Ok(())
170238
}
171239

172240
/// Given a `path` with glob characters in it (e.g. `*.json`), return a vector of matching files

kani-driver/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ fn cargokani_main(input_args: Vec<OsString>) -> Result<()> {
6565
let session = session::KaniSession::new(args.common_opts)?;
6666

6767
if let Some(CargoKaniSubcommand::Assess(args)) = args.command {
68-
return assess::cargokani_assess_main(session, args);
68+
return assess::run_assess(session, args);
6969
} else if session.args.assess {
70-
return assess::cargokani_assess_main(session, assess::AssessArgs::default());
70+
return assess::run_assess(session, assess::AssessArgs::default());
7171
}
7272

7373
let project = project::cargo_project(&session)?;
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
error: Crate crate_with_global_asm contains global ASM, which is not supported by Kani. Rerun with `--enable-unstable --ignore-global-asm` to suppress this error (**Verification results may be impacted**).
2-
error: could not compile `crate_with_global_asm` due to previous error
3-
error: cargo exited with status exit status: 101
2+
error: could not compile `crate_with_global_asm` due to 2 previous errors

tests/cargo-kani/stubbing-do-not-resolve/harness.expected

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ error: unable to resolve function/method: crate::other_crate2::mock
22
error: unable to resolve function/method: super::other_crate2::mock
33
error: unable to resolve function/method: self::other_crate2::mock
44
error: unable to resolve function/method: other_crate1::mock
5-
error: could not compile `stubbing-do-not-resolve` due to 4 previous errors
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright Kani Contributors
2+
# SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
[package]
5+
name = "compilation-error"
6+
version = "0.1.0"
7+
edition = "2021"
8+
9+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10+
11+
[package.metadata.kani]
12+
flags = { assess=true, enable-unstable=true }

tests/cargo-ui/assess-error/expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
error: Failed to assess project: Failed to execute cargo (exit status: 101). Found 3 compilation errors.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//! Check that the compilation error detection works as expected
4+
use std::option;
5+
6+
pub fn add(left: usize, right: u32) -> usize {
7+
left + right
8+
}
9+
10+
#[cfg(test)]
11+
mod tests {
12+
use super::*;
13+
14+
#[test]
15+
fn it_works() {
16+
let result = add(2, 2);
17+
assert_eq!(result, 4);
18+
}
19+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
error: `&str` doesn't implement `DoIt`. The function `foo` cannot be stubbed by `bar` due to generic bounds not being met.
22
error: `&str` doesn't implement `std::cmp::PartialEq`. The function `foo` cannot be stubbed by `bar` due to generic bounds not being met.
3-
error: could not compile `function-stubbing-trait-mismatch` due to 2 previous errors

0 commit comments

Comments
 (0)