Skip to content

Commit c87fb24

Browse files
authored
Create a playback command to make it easier to run Kani generated tests (rust-lang#2464)
Introduce the unstable subcommand playback to allow users to easily compile and run their test cases that were generated using Kani's concrete playback feature.
1 parent 8649942 commit c87fb24

File tree

28 files changed

+877
-161
lines changed

28 files changed

+877
-161
lines changed

kani-driver/src/args/common.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,34 @@ impl ValidateArgs for CommonArgs {
5555
Ok(())
5656
}
5757
}
58+
59+
/// The verbosity level to be used in Kani.
60+
pub trait Verbosity {
61+
/// Whether we should be quiet.
62+
fn quiet(&self) -> bool;
63+
/// Whether we should be verbose.
64+
/// Note that `debug() == true` must imply `verbose() == true`.
65+
fn verbose(&self) -> bool;
66+
/// Whether we should emit debug messages.
67+
fn debug(&self) -> bool;
68+
/// Whether any verbosity was selected.
69+
fn is_set(&self) -> bool;
70+
}
71+
72+
impl Verbosity for CommonArgs {
73+
fn quiet(&self) -> bool {
74+
self.quiet
75+
}
76+
77+
fn verbose(&self) -> bool {
78+
self.verbose || self.debug
79+
}
80+
81+
fn debug(&self) -> bool {
82+
self.debug
83+
}
84+
85+
fn is_set(&self) -> bool {
86+
self.quiet || self.verbose || self.debug
87+
}
88+
}

kani-driver/src/args/mod.rs

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
pub mod assess_args;
66
pub mod common;
7+
pub mod playback_args;
78

89
pub use assess_args::*;
910

@@ -60,14 +61,29 @@ const DEFAULT_OBJECT_BITS: u32 = 16;
6061
version,
6162
name = "kani",
6263
about = "Verify a single Rust crate. For more information, see https://github.com/model-checking/kani",
63-
args_override_self = true
64+
args_override_self = true,
65+
subcommand_negates_reqs = true,
66+
subcommand_precedence_over_arg = true,
67+
args_conflicts_with_subcommands = true
6468
)]
6569
pub struct StandaloneArgs {
6670
/// Rust file to verify
67-
pub input: PathBuf,
71+
#[arg(required = true)]
72+
pub input: Option<PathBuf>,
6873

6974
#[command(flatten)]
7075
pub verify_opts: VerificationArgs,
76+
77+
#[command(subcommand)]
78+
pub command: Option<StandaloneSubcommand>,
79+
}
80+
81+
/// Kani takes optional subcommands to request specialized behavior.
82+
/// When no subcommand is provided, there is an implied verification subcommand.
83+
#[derive(Debug, clap::Subcommand)]
84+
pub enum StandaloneSubcommand {
85+
/// Execute concrete playback testcases of a local crate.
86+
Playback(Box<playback_args::KaniPlaybackArgs>),
7187
}
7288

7389
#[derive(Debug, clap::Parser)]
@@ -85,11 +101,14 @@ pub struct CargoKaniArgs {
85101
pub verify_opts: VerificationArgs,
86102
}
87103

88-
// cargo-kani takes optional subcommands to request specialized behavior
104+
/// cargo-kani takes optional subcommands to request specialized behavior
89105
#[derive(Debug, clap::Subcommand)]
90106
pub enum CargoKaniSubcommand {
91107
#[command(hide = true)]
92108
Assess(Box<crate::assess::AssessArgs>),
109+
110+
/// Execute concrete playback testcases of a local package.
111+
Playback(Box<playback_args::CargoPlaybackArgs>),
93112
}
94113

95114
// Common arguments for invoking Kani for verification purpose. This gets put into KaniContext,
@@ -361,6 +380,42 @@ impl CargoArgs {
361380
}
362381
result
363382
}
383+
384+
/// Convert the arguments back to a format that cargo can understand.
385+
/// Note that the `exclude` option requires special processing and it's not included here.
386+
pub fn to_cargo_args(&self) -> Vec<OsString> {
387+
let mut cargo_args: Vec<OsString> = vec![];
388+
if self.all_features {
389+
cargo_args.push("--all-features".into());
390+
}
391+
392+
if self.no_default_features {
393+
cargo_args.push("--no-default-features".into());
394+
}
395+
396+
let features = self.features();
397+
if !features.is_empty() {
398+
cargo_args.push(format!("--features={}", features.join(",")).into());
399+
}
400+
401+
if let Some(path) = &self.manifest_path {
402+
cargo_args.push("--manifest-path".into());
403+
cargo_args.push(path.into());
404+
}
405+
if self.workspace {
406+
cargo_args.push("--workspace".into())
407+
}
408+
409+
cargo_args.extend(self.package.iter().map(|pkg| format!("-p={pkg}").into()));
410+
cargo_args
411+
}
412+
}
413+
414+
/// Leave it for Cargo to validate these for now.
415+
impl ValidateArgs for CargoArgs {
416+
fn validate(&self) -> Result<(), Error> {
417+
Ok(())
418+
}
364419
}
365420

366421
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
@@ -463,23 +518,44 @@ impl CheckArgs {
463518
impl ValidateArgs for StandaloneArgs {
464519
fn validate(&self) -> Result<(), Error> {
465520
self.verify_opts.validate()?;
466-
if !self.input.is_file() {
467-
Err(Error::raw(
468-
ErrorKind::InvalidValue,
469-
&format!(
470-
"Invalid argument: Input invalid. `{}` is not a regular file.",
471-
self.input.display()
472-
),
473-
))
474-
} else {
475-
Ok(())
521+
if let Some(input) = &self.input {
522+
if !input.is_file() {
523+
return Err(Error::raw(
524+
ErrorKind::InvalidValue,
525+
&format!(
526+
"Invalid argument: Input invalid. `{}` is not a regular file.",
527+
input.display()
528+
),
529+
));
530+
}
531+
}
532+
Ok(())
533+
}
534+
}
535+
536+
impl<T> ValidateArgs for Option<T>
537+
where
538+
T: ValidateArgs,
539+
{
540+
fn validate(&self) -> Result<(), Error> {
541+
self.as_ref().map_or(Ok(()), |inner| inner.validate())
542+
}
543+
}
544+
545+
impl ValidateArgs for CargoKaniSubcommand {
546+
fn validate(&self) -> Result<(), Error> {
547+
match self {
548+
// Assess doesn't implement validation yet.
549+
CargoKaniSubcommand::Assess(_) => Ok(()),
550+
CargoKaniSubcommand::Playback(playback) => playback.validate(),
476551
}
477552
}
478553
}
479554

480555
impl ValidateArgs for CargoKaniArgs {
481556
fn validate(&self) -> Result<(), Error> {
482557
self.verify_opts.validate()?;
558+
self.command.validate()?;
483559
// --assess requires --enable-unstable, but the subcommand needs manual checking
484560
if (matches!(self.command, Some(CargoKaniSubcommand::Assess(_))) || self.verify_opts.assess)
485561
&& !self.verify_opts.common_args.enable_unstable
@@ -886,4 +962,12 @@ mod tests {
886962
assert_eq!(parse(&["kani", "--features", "a", "--features", "b,c"]), ["a", "b", "c"]);
887963
assert_eq!(parse(&["kani", "--features", "a b", "-Fc"]), ["a", "b", "c"]);
888964
}
965+
966+
#[test]
967+
fn check_kani_playback() {
968+
let input = "kani playback --test dummy file.rs".split_whitespace();
969+
let args = StandaloneArgs::try_parse_from(input).unwrap();
970+
assert_eq!(args.input, None);
971+
assert!(matches!(args.command, Some(StandaloneSubcommand::Playback(..))));
972+
}
889973
}

kani-driver/src/args/playback_args.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//! Implements the subcommand handling of the playback subcommand
4+
5+
use crate::args::common::UnstableFeatures;
6+
use crate::args::{CargoArgs, CommonArgs, ValidateArgs};
7+
use clap::error::ErrorKind;
8+
use clap::{Error, Parser, ValueEnum};
9+
use std::path::PathBuf;
10+
11+
/// Execute concrete playback testcases of a local package.
12+
#[derive(Debug, Parser)]
13+
pub struct CargoPlaybackArgs {
14+
#[command(flatten)]
15+
pub playback: PlaybackArgs,
16+
17+
/// Arguments to pass down to Cargo
18+
#[command(flatten)]
19+
pub cargo: CargoArgs,
20+
}
21+
22+
/// Execute concrete playback testcases of a local crate.
23+
#[derive(Debug, Parser)]
24+
pub struct KaniPlaybackArgs {
25+
/// Rust crate's top file location.
26+
pub input: PathBuf,
27+
28+
#[command(flatten)]
29+
pub playback: PlaybackArgs,
30+
}
31+
32+
/// Playback subcommand arguments.
33+
#[derive(Debug, clap::Args)]
34+
pub struct PlaybackArgs {
35+
/// Common args always available to Kani subcommands.
36+
#[command(flatten)]
37+
pub common_opts: CommonArgs,
38+
39+
/// Specify which test will be executed by the playback args.
40+
#[arg(long)]
41+
pub test: String,
42+
43+
/// Compile but don't actually run the tests.
44+
#[arg(long)]
45+
pub only_codegen: bool,
46+
47+
// TODO: We should make this a common option to all subcommands.
48+
/// Control the subcommand output.
49+
#[arg(long, default_value = "human")]
50+
pub message_format: MessageFormat,
51+
}
52+
53+
/// Message formats available for the subcommand.
54+
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, strum_macros::Display)]
55+
#[strum(serialize_all = "kebab-case")]
56+
pub enum MessageFormat {
57+
/// Print diagnostic messages in a user friendly format.
58+
Human,
59+
/// Print diagnostic messages in JSON format.
60+
Json,
61+
}
62+
63+
impl ValidateArgs for CargoPlaybackArgs {
64+
fn validate(&self) -> Result<(), Error> {
65+
self.playback.validate()?;
66+
self.cargo.validate()
67+
}
68+
}
69+
70+
impl ValidateArgs for KaniPlaybackArgs {
71+
fn validate(&self) -> Result<(), Error> {
72+
self.playback.validate()?;
73+
if !self.input.is_file() {
74+
return Err(Error::raw(
75+
ErrorKind::InvalidValue,
76+
&format!(
77+
"Invalid argument: Input invalid. `{}` is not a regular file.",
78+
self.input.display()
79+
),
80+
));
81+
}
82+
Ok(())
83+
}
84+
}
85+
86+
impl ValidateArgs for PlaybackArgs {
87+
fn validate(&self) -> Result<(), Error> {
88+
self.common_opts.validate()?;
89+
if !self.common_opts.unstable_features.contains(&UnstableFeatures::ConcretePlayback) {
90+
return Err(Error::raw(
91+
ErrorKind::MissingRequiredArgument,
92+
"The `playback` subcommand is unstable and requires `-Z concrete-playback` \
93+
to be used.",
94+
));
95+
}
96+
Ok(())
97+
}
98+
}
99+
100+
#[cfg(test)]
101+
mod tests {
102+
use clap::Parser;
103+
104+
use super::*;
105+
106+
#[test]
107+
fn check_cargo_parse_test_works() {
108+
let input = "playback -Z concrete-playback --test TEST_NAME".split_whitespace();
109+
let args = CargoPlaybackArgs::try_parse_from(input.clone()).unwrap();
110+
args.validate().unwrap();
111+
assert_eq!(args.playback.test, "TEST_NAME");
112+
// The default value is human friendly.
113+
assert_eq!(args.playback.message_format, MessageFormat::Human);
114+
}
115+
116+
#[test]
117+
fn check_cargo_parse_pkg_works() {
118+
let input = "playback -Z concrete-playback --test TEST_NAME -p PKG_NAME".split_whitespace();
119+
let args = CargoPlaybackArgs::try_parse_from(input).unwrap();
120+
args.validate().unwrap();
121+
assert_eq!(args.playback.test, "TEST_NAME");
122+
assert_eq!(&args.cargo.package, &["PKG_NAME"])
123+
}
124+
125+
#[test]
126+
fn check_parse_format_works() {
127+
let input = "playback -Z concrete-playback --test TEST_NAME --message-format=json"
128+
.split_whitespace();
129+
let args = CargoPlaybackArgs::try_parse_from(input).unwrap();
130+
args.validate().unwrap();
131+
assert_eq!(args.playback.test, "TEST_NAME");
132+
assert_eq!(args.playback.message_format, MessageFormat::Json)
133+
}
134+
135+
#[test]
136+
fn check_kani_parse_test_works() {
137+
let input = "playback -Z concrete-playback --test TEST_NAME input.rs".split_whitespace();
138+
let args = KaniPlaybackArgs::try_parse_from(input).unwrap();
139+
// Don't validate this since we check if the input file exists.
140+
//args.validate().unwrap();
141+
assert_eq!(args.playback.test, "TEST_NAME");
142+
assert_eq!(args.input, PathBuf::from("input.rs"));
143+
// The default value is human friendly.
144+
assert_eq!(args.playback.message_format, MessageFormat::Human);
145+
}
146+
147+
#[test]
148+
fn check_kani_no_unstable_fails() {
149+
let input = "playback --test TEST_NAME input.rs".split_whitespace();
150+
let args = KaniPlaybackArgs::try_parse_from(input).unwrap();
151+
let err = args.validate().unwrap_err();
152+
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
153+
}
154+
}

0 commit comments

Comments
 (0)