Skip to content

Commit b85813e

Browse files
celinvaladpaco-aws
andauthored
Only generate tests if the failure is reproducible (rust-lang#1629)
* Only generate tests if the failure is reproducible The concrete playback feature was generating an empty test for cases where the verification failed due to some non-reproducible error. For example, unwind loop assertion failure. Instead, we print a warning if the verification failure doesn't include any reproducible trace. * Address feedback and polish arg handling - Create validate_inner() to allow us to unit test the argument parsing. Co-authored-by: Adrian Palacios <[email protected]>
1 parent dc2cf56 commit b85813e

File tree

4 files changed

+147
-136
lines changed

4 files changed

+147
-136
lines changed

kani-driver/src/args.rs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ impl KaniArgs {
222222
}
223223

224224
arg_enum! {
225-
#[derive(Debug, PartialEq, Eq)]
225+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
226226
pub enum ConcretePlaybackMode {
227227
Print,
228228
InPlace,
@@ -343,26 +343,39 @@ impl CargoKaniArgs {
343343
}
344344
impl KaniArgs {
345345
pub fn validate(&self) {
346+
self.validate_inner().or_else(|e| -> Result<(), ()> { e.exit() }).unwrap()
347+
}
348+
349+
fn validate_inner(&self) -> Result<(), Error> {
346350
let extra_unwind =
347351
self.cbmc_args.iter().any(|s| s.to_str().unwrap().starts_with("--unwind"));
348352
let natives_unwind = self.default_unwind.is_some() || self.unwind.is_some();
349353

350354
// TODO: these conflicting flags reflect what's necessary to pass current tests unmodified.
351355
// We should consider improving the error messages slightly in a later pull request.
352356
if natives_unwind && extra_unwind {
353-
Error::with_description(
357+
Err(Error::with_description(
354358
"Conflicting flags: unwind flags provided to kani and in --cbmc-args.",
355359
ErrorKind::ArgumentConflict,
356-
)
357-
.exit();
358-
}
359-
360-
if self.cbmc_args.contains(&OsString::from("--function")) {
361-
Error::with_description(
360+
))
361+
} else if self.cbmc_args.contains(&OsString::from("--function")) {
362+
Err(Error::with_description(
362363
"Invalid flag: --function should be provided to Kani directly, not via --cbmc-args.",
363364
ErrorKind::ArgumentConflict,
364-
)
365-
.exit();
365+
))
366+
} else if self.quiet && self.concrete_playback == Some(ConcretePlaybackMode::Print) {
367+
Err(Error::with_description(
368+
"Conflicting options: --concrete-playback=print and --quiet.",
369+
ErrorKind::ArgumentConflict,
370+
))
371+
} else if self.concrete_playback.is_some() && self.output_format == OutputFormat::Old {
372+
Err(Error::with_description(
373+
"Conflicting options: --concrete-playback isn't compatible with \
374+
--output-format=old.",
375+
ErrorKind::ArgumentConflict,
376+
))
377+
} else {
378+
Ok(())
366379
}
367380
}
368381
}
@@ -462,4 +475,28 @@ mod tests {
462475
fn check_disable_slicing_unstable() {
463476
check_unstable_flag("--no-slice-formula")
464477
}
478+
479+
#[test]
480+
fn check_concrete_playback_unstable() {
481+
check_unstable_flag("--concrete-playback inplace");
482+
check_unstable_flag("--concrete-playback print");
483+
}
484+
485+
/// Check if parsing the given argument string results in the given error.
486+
fn expect_validation_error(arg: &str, err: ErrorKind) {
487+
let args = StandaloneArgs::from_iter(arg.split_whitespace());
488+
assert_eq!(args.common_opts.validate_inner().unwrap_err().kind, err);
489+
}
490+
491+
#[test]
492+
fn check_concrete_playback_conflicts() {
493+
expect_validation_error(
494+
"kani --concrete-playback=print --quiet --enable-unstable test.rs",
495+
ErrorKind::ArgumentConflict,
496+
);
497+
expect_validation_error(
498+
"kani --concrete-playback=inplace --output-format=old --enable-unstable test.rs",
499+
ErrorKind::ArgumentConflict,
500+
);
501+
}
465502
}

kani-driver/src/concrete_playback.rs

Lines changed: 84 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use crate::args::ConcretePlaybackMode;
88
use crate::call_cbmc::VerificationStatus;
99
use crate::cbmc_output_parser::VerificationOutput;
1010
use crate::session::KaniSession;
11-
use anyhow::{ensure, Context, Result};
12-
use concrete_vals_extractor::{extract_from_processed_items, ConcreteVal};
11+
use anyhow::{Context, Result};
12+
use concrete_vals_extractor::{extract_harness_values, ConcreteVal};
1313
use kani_metadata::HarnessMetadata;
1414
use std::collections::hash_map::DefaultHasher;
1515
use std::ffi::OsString;
@@ -26,54 +26,52 @@ impl KaniSession {
2626
harness: &HarnessMetadata,
2727
verification_output: &VerificationOutput,
2828
) -> Result<()> {
29-
let playback_mode = match &self.args.concrete_playback {
29+
let playback_mode = match self.args.concrete_playback {
3030
Some(playback_mode) => playback_mode,
3131
None => return Ok(()),
3232
};
3333

34-
ensure!(
35-
self.args.output_format != crate::args::OutputFormat::Old,
36-
"The Kani argument `--output-format old` is not supported with the concrete playback feature."
37-
);
38-
3934
if verification_output.status == VerificationStatus::Success {
4035
return Ok(());
4136
}
4237

4338
if let Some(processed_items) = &verification_output.processed_items {
44-
let concrete_vals = extract_from_processed_items(processed_items).expect(
45-
"Something went wrong when trying to get concrete values from the CBMC output",
46-
);
47-
let concrete_playback = format_unit_test(&harness.mangled_name, &concrete_vals);
48-
49-
if *playback_mode == ConcretePlaybackMode::Print {
50-
ensure!(
51-
!self.args.quiet,
52-
"With `--quiet` mode enabled, `--concrete-playback=print` mode can not print test cases."
53-
);
54-
println!(
55-
"Concrete playback unit test for `{}`:\n```\n{}\n```",
56-
&harness.mangled_name, &concrete_playback.unit_test_str
57-
);
58-
println!(
59-
"INFO: To automatically add the concrete playback unit test `{}` to the src code, run Kani with `--concrete-playback=InPlace`.",
60-
&concrete_playback.unit_test_name
61-
);
62-
}
63-
64-
if *playback_mode == ConcretePlaybackMode::InPlace {
65-
if !self.args.quiet {
66-
println!(
67-
"INFO: Now modifying the source code to include the concrete playback unit test `{}`.",
68-
&concrete_playback.unit_test_name
69-
);
39+
match extract_harness_values(processed_items) {
40+
None => println!(
41+
"WARNING: Kani could not produce a concrete playback for `{}` because there \
42+
were no failing panic checks.",
43+
harness.pretty_name
44+
),
45+
Some(concrete_vals) => {
46+
let concrete_playback = format_unit_test(&harness.mangled_name, &concrete_vals);
47+
match playback_mode {
48+
ConcretePlaybackMode::Print => {
49+
println!(
50+
"Concrete playback unit test for `{}`:\n```\n{}\n```",
51+
&harness.pretty_name, &concrete_playback.unit_test_str
52+
);
53+
println!(
54+
"INFO: To automatically add the concrete playback unit test `{}` to the \
55+
src code, run Kani with `--concrete-playback=inplace`.",
56+
&concrete_playback.unit_test_name
57+
);
58+
}
59+
ConcretePlaybackMode::InPlace => {
60+
if !self.args.quiet {
61+
println!(
62+
"INFO: Now modifying the source code to include the concrete playback unit test `{}`.",
63+
&concrete_playback.unit_test_name
64+
);
65+
}
66+
self.modify_src_code(
67+
&harness.original_file,
68+
harness.original_end_line,
69+
&concrete_playback,
70+
)
71+
.expect("Failed to modify source code");
72+
}
73+
}
7074
}
71-
self.modify_src_code(
72-
&harness.original_file,
73-
harness.original_end_line,
74-
&concrete_playback,
75-
)
76-
.expect("Failed to modify source code");
7775
}
7876
}
7977
Ok(())
@@ -290,89 +288,53 @@ struct UnitTest {
290288
/// ..., ] }
291289
/// ```
292290
mod concrete_vals_extractor {
293-
use crate::cbmc_output_parser::{
294-
extract_property_class, CheckStatus, ParserItem, Property, TraceItem,
295-
};
296-
use anyhow::{bail, ensure, Context, Result};
291+
use crate::cbmc_output_parser::{extract_property_class, CheckStatus, ParserItem, TraceItem};
297292

298293
pub struct ConcreteVal {
299294
pub byte_arr: Vec<u8>,
300295
pub interp_val: String,
301296
}
302297

303-
/// The first-level extractor. Traverses processed items to find properties.
304-
pub fn extract_from_processed_items(
305-
processed_items: &[ParserItem],
306-
) -> Result<Vec<ConcreteVal>> {
307-
let mut concrete_vals: Vec<ConcreteVal> = Vec::new();
308-
let mut extracted_assert_fail = false;
309-
let result_item = extract_result_from_processed_items(processed_items)?;
310-
for property in result_item {
311-
// Even after extracting an assert fail, we continue to call extract on more properties to provide
312-
// better diagnostics to the user in case they expected even future checks to be extracted.
313-
let old_extracted_assert_fail = extracted_assert_fail;
314-
let new_concrete_vals = extract_from_property(property, &mut extracted_assert_fail)?;
315-
if !old_extracted_assert_fail && extracted_assert_fail {
316-
concrete_vals = new_concrete_vals;
317-
}
318-
}
319-
Ok(concrete_vals)
320-
}
321-
322-
/// Extracts the result item from all the processed items. No result item means that there is an error.
323-
fn extract_result_from_processed_items(processed_items: &[ParserItem]) -> Result<&[Property]> {
324-
for processed_item in processed_items {
325-
if let ParserItem::Result { result } = processed_item {
326-
return Ok(result);
327-
}
328-
}
329-
bail!("No result item found in processed items.")
330-
}
331-
332-
/// The second-level extractor. Traverses properties to find trace items.
333-
fn extract_from_property(
334-
property: &Property,
335-
extracted_assert_fail: &mut bool,
336-
) -> Result<Vec<ConcreteVal>> {
337-
let mut concrete_vals: Vec<ConcreteVal> = Vec::new();
338-
let property_class =
339-
extract_property_class(property).context("Incorrectly formatted property class.")?;
340-
let property_is_assert = property_class == "assertion";
341-
let status_is_failure = property.status == CheckStatus::Failure;
342-
343-
if property_is_assert && status_is_failure {
344-
if *extracted_assert_fail {
298+
/// Extract a set of concrete values that trigger one assertion failure.
299+
/// This will return None if the failure is not related to a user assertion.
300+
pub fn extract_harness_values(processed_items: &[ParserItem]) -> Option<Vec<ConcreteVal>> {
301+
let result_item =
302+
processed_items
303+
.iter()
304+
.find_map(|item| {
305+
if let ParserItem::Result { result } = item { Some(result) } else { None }
306+
})
307+
.expect("Missing CBMC result.");
308+
309+
let mut failures = result_item.iter().filter(|prop| {
310+
extract_property_class(prop).expect("Unexpected property class.") == "assertion"
311+
&& prop.status == CheckStatus::Failure
312+
});
313+
314+
// Process the first assertion failure.
315+
let first_failure = failures.next();
316+
if let Some(property) = first_failure {
317+
// Extract values for the first assertion that has failed.
318+
let trace =
319+
property.trace.as_ref().expect(&format!("Missing trace for {}", property.property));
320+
let concrete_vals = trace.iter().filter_map(&extract_from_trace_item).collect();
321+
322+
// Print warnings for all the other failures that were not handled in case they expected
323+
// even future checks to be extracted.
324+
for unhandled in failures {
345325
println!(
346326
"WARNING: Unable to extract concrete values from multiple failing assertions. Skipping property `{}` with description `{}`.",
347-
property.property, property.description,
348-
);
349-
} else {
350-
*extracted_assert_fail = true;
351-
println!(
352-
"INFO: Parsing concrete values from property `{}` with description `{}`.",
353-
property.property, property.description,
327+
unhandled.property, unhandled.description,
354328
);
355-
if let Some(trace) = &property.trace {
356-
for trace_item in trace {
357-
let concrete_val_opt = extract_from_trace_item(trace_item)
358-
.context("Failure in trace assignment expression:")?;
359-
if let Some(concrete_val) = concrete_val_opt {
360-
concrete_vals.push(concrete_val);
361-
}
362-
}
363-
}
364329
}
365-
} else if !property_is_assert && status_is_failure {
366-
println!(
367-
"WARNING: Unable to extract concrete values from failing non-assertion checks. Skipping property `{}` with description `{}`.",
368-
property.property, property.description,
369-
);
330+
Some(concrete_vals)
331+
} else {
332+
None
370333
}
371-
Ok(concrete_vals)
372334
}
373335

374-
/// The third-level extractor. Extracts individual bytes from kani::any calls.
375-
fn extract_from_trace_item(trace_item: &TraceItem) -> Result<Option<ConcreteVal>> {
336+
/// Extracts individual bytes returned by kani::any() calls.
337+
fn extract_from_trace_item(trace_item: &TraceItem) -> Option<ConcreteVal> {
376338
if let (Some(lhs), Some(source_location), Some(value)) =
377339
(&trace_item.lhs, &trace_item.source_location, &trace_item.value)
378340
{
@@ -389,37 +351,33 @@ mod concrete_vals_extractor {
389351
{
390352
let declared_width = width_u64 as usize;
391353
let actual_width = bit_concrete_val.len();
392-
ensure!(
393-
declared_width == actual_width,
394-
format!(
395-
"Declared width of {declared_width} doesn't equal actual width of {actual_width}"
396-
)
354+
assert_eq!(
355+
declared_width, actual_width,
356+
"Declared width of {declared_width} doesn't equal actual width of {actual_width}"
397357
);
398358
let mut next_num: Vec<u8> = Vec::new();
399359

400360
// Reverse because of endianess of CBMC trace.
401361
for i in (0..declared_width).step_by(8).rev() {
402362
let str_chunk = &bit_concrete_val[i..i + 8];
403363
let str_chunk_len = str_chunk.len();
404-
ensure!(
405-
str_chunk_len == 8,
406-
format!(
407-
"Tried to read a chunk of 8 bits of actually read {str_chunk_len} bits"
408-
)
364+
assert_eq!(
365+
str_chunk_len, 8,
366+
"Tried to read a chunk of 8 bits of actually read {str_chunk_len} bits"
409367
);
410-
let next_byte = u8::from_str_radix(str_chunk, 2).with_context(|| {
411-
format!("Couldn't convert the string chunk `{str_chunk}` to u8")
412-
})?;
368+
let next_byte = u8::from_str_radix(str_chunk, 2).expect(&format!(
369+
"Couldn't convert the string chunk `{str_chunk}` to u8"
370+
));
413371
next_num.push(next_byte);
414372
}
415373

416-
return Ok(Some(ConcreteVal {
374+
return Some(ConcreteVal {
417375
byte_arr: next_num,
418376
interp_val: interp_concrete_val.to_string(),
419-
}));
377+
});
420378
}
421379
}
422380
}
423-
Ok(None)
381+
None
424382
}
425383
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Failed Checks: unwinding assertion loop 0
2+
WARNING: Kani could not produce a concrete playback for `check_unwind_fail` because there were no failing panic checks.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
// kani-flags: --enable-unstable --concrete-playback=print
5+
6+
#[kani::proof]
7+
#[kani::unwind(2)]
8+
fn check_unwind_fail() {
9+
let mut cnt = 0;
10+
while kani::any() {
11+
cnt += 1;
12+
assert!(cnt < 10);
13+
}
14+
}

0 commit comments

Comments
 (0)