Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] kani-cov: A coverage tool for Kani #3121

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c487f2a
`kanicov`: A tool to visualize coverage in Kani
adpaco-aws Apr 3, 2024
eddb363
Rename and more
adpaco-aws Apr 9, 2024
935dace
Merge branch 'main' into kanicov-tool
adpaco-aws Aug 21, 2024
13c26fd
Scaffold for `kani-cov`
adpaco-aws Aug 22, 2024
bee8f2d
Parse and combine raw results
adpaco-aws Aug 22, 2024
e0b7a10
Merge branch 'main' into kanicov-tool
adpaco-aws Aug 28, 2024
0aa25b9
Add missing copyright notices
adpaco-aws Aug 28, 2024
debeef4
Remove `atty` dependency
adpaco-aws Aug 29, 2024
9e9575a
Add license to Cargo.toml
adpaco-aws Aug 29, 2024
dd53e38
Move unused code to report
adpaco-aws Aug 29, 2024
ad632f3
Save combined results through merge
adpaco-aws Aug 29, 2024
a659c1a
Start summary command
adpaco-aws Aug 29, 2024
76351fd
Some format fixes
adpaco-aws Aug 29, 2024
304cfd2
Fixes for `clippy`
adpaco-aws Aug 29, 2024
c69f465
Add description to Cargo.toml
adpaco-aws Aug 29, 2024
d0bb478
Parse function info with tree-sitter
adpaco-aws Aug 30, 2024
5512d28
Use file-function pairs to combine results
adpaco-aws Aug 31, 2024
0e1dcc0
Change combined results again
adpaco-aws Aug 31, 2024
fabdf44
Produce markdown table with `summary`
adpaco-aws Sep 13, 2024
855d0a6
Sketch Json format for summary
adpaco-aws Sep 13, 2024
0df18b9
Refator some code so it's shared with `report`
adpaco-aws Sep 13, 2024
50bb553
Init. version for terminal reports
adpaco-aws Sep 16, 2024
5b4d717
Simplify processing for markers
adpaco-aws Sep 17, 2024
e1d0160
Remove some formatting code that's not needed anymore
adpaco-aws Sep 17, 2024
6d47c0b
Remove leftover debugging info
adpaco-aws Sep 17, 2024
ae48e21
Complete terminal highlight algorithm
adpaco-aws Sep 19, 2024
8c79baf
Change `coverage` mode to get `kani-cov` reports
adpaco-aws Sep 19, 2024
39f5ac2
Bless expected files
adpaco-aws Sep 19, 2024
778816a
Complete blessing for `coverage` tests
adpaco-aws Sep 19, 2024
b33fd5c
Merge branch 'main' into kanicov-tool
adpaco-aws Sep 20, 2024
06b97f0
Reformat
adpaco-aws Sep 20, 2024
781ba58
Clippy fixes
adpaco-aws Sep 20, 2024
7d7ef6c
Clean up: unused imports and commented out code
adpaco-aws Sep 20, 2024
a997656
Merge branch 'main' into kanicov-tool
adpaco-aws Sep 20, 2024
5cd0687
Report format
adpaco-aws Sep 20, 2024
4694294
Introduce format for reports
adpaco-aws Sep 20, 2024
21f2bd8
Add documentation for several functions
adpaco-aws Sep 20, 2024
923e606
Most clippy fixes
adpaco-aws Sep 20, 2024
812231e
More documentation
adpaco-aws Sep 20, 2024
745a55f
Documentation for main methods
adpaco-aws Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -977,9 +977,9 @@ dependencies = [

[[package]]
name = "serde_json"
version = "1.0.114"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
Expand Down Expand Up @@ -1317,6 +1317,17 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "visualize_coverage"
version = "0.1.0"
dependencies = [
"anyhow",
"console",
"serde",
"serde_derive",
"serde_json",
]

[[package]]
name = "wait-timeout"
version = "0.2.0"
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ members = [
"tools/bookrunner",
"tools/compiletest",
"tools/build-kani",
"tools/kanicov",
adpaco-aws marked this conversation as resolved.
Show resolved Hide resolved
"kani-driver",
"kani-compiler",
"kani_metadata",
Expand Down
13 changes: 13 additions & 0 deletions tools/kanicov/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "visualize_coverage"
adpaco-aws marked this conversation as resolved.
Show resolved Hide resolved
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.81"
console = "0.15.8"
serde = "1.0.197"
serde_derive = "1.0.197"
serde_json = "1.0.115"
124 changes: 124 additions & 0 deletions tools/kanicov/src/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use anyhow::Result;
use console::style;
use serde_derive::{Deserialize, Serialize};
use std::fmt::{self, Write};
use std::{collections::BTreeMap, fmt::Display};

#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
pub enum CheckStatus {
Failure,
Covered, // for `code_coverage` properties only
Satisfied, // for `cover` properties only
Success,
Undetermined,
Unreachable,
Uncovered, // for `code_coverage` properties only
Unsatisfiable, // for `cover` properties only
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CoverageResults {
pub data: BTreeMap<String, Vec<CoverageCheck>>,
}

impl CoverageResults {
pub fn new(data: BTreeMap<String, Vec<CoverageCheck>>) -> Self {
Self { data }
}
}
pub fn fmt_coverage_results(coverage_results: &CoverageResults) -> Result<String> {
let mut fmt_string = String::new();
for (file, checks) in coverage_results.data.iter() {
let mut checks_by_function: BTreeMap<String, Vec<CoverageCheck>> = BTreeMap::new();

// // Group checks by function
for check in checks {
// Insert the check into the vector corresponding to its function
checks_by_function
.entry(check.function.clone())
.or_insert_with(Vec::new)
.push(check.clone());
}

for (function, checks) in checks_by_function {
writeln!(fmt_string, "{file} ({function})")?;
let mut sorted_checks: Vec<CoverageCheck> = checks.to_vec();
sorted_checks.sort_by(|a, b| a.region.start.cmp(&b.region.start));
for check in sorted_checks.iter() {
writeln!(fmt_string, " * {} {}", check.region, check.status)?;
}
writeln!(fmt_string, "")?;
}
}
Ok(fmt_string)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageCheck {
pub function: String,
term: CoverageTerm,
pub region: CoverageRegion,
status: CheckStatus,
}

impl CoverageCheck {
pub fn new(
function: String,
term: CoverageTerm,
region: CoverageRegion,
status: CheckStatus,
) -> Self {
Self { function, term, region, status }
}

pub fn is_covered(&self) -> bool {
self.status == CheckStatus::Covered
}
}

#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CoverageRegion {
pub file: String,
pub start: (u32, u32),
pub end: (u32, u32),
}

impl Display for CoverageRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{} - {}:{}", self.start.0, self.start.1, self.end.0, self.end.1)
}
}

impl CoverageRegion {
pub fn from_str(str: String) -> Self {
let str_splits: Vec<&str> = str.split([':', '-']).map(|s| s.trim()).collect();
assert_eq!(str_splits.len(), 5, "{str:?}");
let file = str_splits[0].to_string();
let start = (str_splits[1].parse().unwrap(), str_splits[2].parse().unwrap());
let end = (str_splits[3].parse().unwrap(), str_splits[4].parse().unwrap());
Self { file, start, end }
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CoverageTerm {
Counter(u32),
Expression(u32),
}

impl std::fmt::Display for CheckStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let check_str = match self {
CheckStatus::Satisfied => style("SATISFIED").green(),
CheckStatus::Success => style("SUCCESS").green(),
CheckStatus::Covered => style("COVERED").green(),
CheckStatus::Uncovered => style("UNCOVERED").red(),
CheckStatus::Failure => style("FAILURE").red(),
CheckStatus::Unreachable => style("UNREACHABLE").yellow(),
CheckStatus::Undetermined => style("UNDETERMINED").yellow(),
CheckStatus::Unsatisfiable => style("UNSATISFIABLE").yellow(),
};
write!(f, "{check_str}")
}
}
113 changes: 113 additions & 0 deletions tools/kanicov/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
mod coverage;
use std::fs::File;
use std::io::Read;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;

use crate::coverage::CoverageResults;
use anyhow::{Context, Result};
use coverage::CoverageCheck;

fn main() -> Result<()> {
let args = std::env::args().collect::<Vec<_>>();

let file_path = args.get(1).context("kanicov file not specified")?;
let mut file = File::open(file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let path_to_root = cargo_root_dir(file_path.into());
let cov_results: CoverageResults = serde_json::from_str(&contents)?;
let visual_results = visualize_coverage_results(&cov_results, path_to_root.unwrap())
.expect("error: couldn't format coverage results");
println!("{visual_results}");
Ok(())
}

fn visualize_coverage_results(cov_results: &CoverageResults, root_path: PathBuf) -> Result<String> {
let mut formatted_output = String::new();
let cov_data = &cov_results.data;

for file in cov_data.keys() {
let file_path = root_path.join(file);
let file_handle = File::open(file_path)?;
let reader = BufReader::new(file_handle);

let checks = cov_data.get(file).unwrap().to_vec();
let mut must_highlight = false;

for (idx, line) in reader.lines().enumerate() {
let line = format!("{}\n", line.unwrap());

let cur_idx = idx + 1;
let line_checks: Vec<&CoverageCheck> = checks
.iter()
.filter(|c| {
c.is_covered()
&& (cur_idx == c.region.start.0 as usize
|| cur_idx == c.region.end.0 as usize)
})
.collect();
let new_line = if line_checks.is_empty() {
if must_highlight {
insert_escapes(&line, vec![(0, true), (line.len() - 1, false)])
} else {
line
}
} else {
let mut markers = vec![];
if must_highlight {
markers.push((0, true))
};

for check in line_checks {
let start_line = check.region.start.0 as usize;
let start_column = (check.region.start.1 - 1u32) as usize;
let end_line = check.region.end.0 as usize;
let end_column = (check.region.end.1 - 1u32) as usize;
if start_line == cur_idx {
markers.push((start_column, true))
}
if end_line == cur_idx {
markers.push((end_column, false))
}
}

if markers.last().unwrap().1 {
must_highlight = true;
markers.push((line.len() - 1, false))
} else {
must_highlight = false;
}
println!("{:?}", markers);
insert_escapes(&line, markers)
};
formatted_output.push_str(&new_line);
}
}
Ok(formatted_output)
}

fn cargo_root_dir(filepath: PathBuf) -> Option<PathBuf> {
let mut cargo_root_path = filepath.clone();
while !cargo_root_path.join("Cargo.toml").exists() {
let pop_result = cargo_root_path.pop();
if !pop_result {
return None;
}
}
Some(cargo_root_path)
}

fn insert_escapes(str: &String, markers: Vec<(usize, bool)>) -> String {
let mut new_str = str.clone();
let mut offset = 0;

let sym_markers = markers.iter().map(|(i, b)| (i, if *b { "\x1b[42m" } else { "\x1b[0m" }));
// let sym_markers = markers.iter().map(|(i, b)| (i, if *b { "```" } else { "'''" }));
for (i, b) in sym_markers {
println!("{}", i + offset);
new_str.insert_str(i + offset, b);
offset = offset + b.bytes().len();
}
new_str
}
Loading