mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-18 12:48:07 +00:00
Add source coverage file format (#1403)
This commit is contained in:
@ -21,6 +21,7 @@ pub mod code;
|
|||||||
pub mod demangle;
|
pub mod demangle;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod sancov;
|
pub mod sancov;
|
||||||
|
pub mod source;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod disasm;
|
pub mod disasm;
|
||||||
|
445
src/agent/coverage/src/source.rs
Normal file
445
src/agent/coverage/src/source.rs
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct SourceCoverage {
|
||||||
|
pub files: Vec<SourceFileCoverage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub struct SourceFileCoverage {
|
||||||
|
/// UTF-8 encoding of the path to the source file.
|
||||||
|
pub file: String,
|
||||||
|
|
||||||
|
pub locations: Vec<SourceCoverageLocation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub struct SourceCoverageLocation {
|
||||||
|
/// Line number of entry in `file` (1-indexed).
|
||||||
|
pub line: u32,
|
||||||
|
|
||||||
|
/// Optional column offset (0-indexed).
|
||||||
|
///
|
||||||
|
/// When column offsets are present, they should be interpreted as the start
|
||||||
|
/// of a span bounded by the next in-line column offset (or end-of-line).
|
||||||
|
pub column: Option<u32>,
|
||||||
|
|
||||||
|
/// Execution count at location.
|
||||||
|
pub count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceCoverageLocation {
|
||||||
|
pub fn new(line: u32, column: impl Into<Option<u32>>, count: u32) -> Result<Self> {
|
||||||
|
if line == 0 {
|
||||||
|
anyhow::bail!("source lines must be 1-indexed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = column.into();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const MAIN_C: &str = "src/bin/main.c";
|
||||||
|
const COMMON_C: &str = "src/lib/common.c";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_location() -> Result<()> {
|
||||||
|
let valid = SourceCoverageLocation::new(5, 4, 1)?;
|
||||||
|
assert_eq!(
|
||||||
|
valid,
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: Some(4),
|
||||||
|
count: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let valid_no_col = SourceCoverageLocation::new(5, None, 1)?;
|
||||||
|
assert_eq!(
|
||||||
|
valid_no_col,
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let invalid = SourceCoverageLocation::new(0, 4, 1);
|
||||||
|
assert!(invalid.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_full() -> Result<()> {
|
||||||
|
let text = serde_json::to_string(&json!([
|
||||||
|
{
|
||||||
|
"file": MAIN_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 4, "column": 4, "count": 1 },
|
||||||
|
{ "line": 9, "column": 4, "count": 0 },
|
||||||
|
{ "line": 12, "column": 4, "count": 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": COMMON_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 5, "column": 4, "count": 0 },
|
||||||
|
{ "line": 5, "column": 9, "count": 1 },
|
||||||
|
{ "line": 8, "column": 0, "count": 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]))?;
|
||||||
|
|
||||||
|
let coverage = {
|
||||||
|
let files = vec![
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: MAIN_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 4,
|
||||||
|
column: Some(4),
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 9,
|
||||||
|
column: Some(4),
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 12,
|
||||||
|
column: Some(4),
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: COMMON_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: Some(4),
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: Some(9),
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 8,
|
||||||
|
column: Some(0),
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
SourceCoverage { files }
|
||||||
|
};
|
||||||
|
|
||||||
|
let ser = serde_json::to_string(&coverage)?;
|
||||||
|
assert_eq!(ser, text);
|
||||||
|
|
||||||
|
let de: SourceCoverage = serde_json::from_str(&text)?;
|
||||||
|
assert_eq!(de, coverage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_no_files() -> Result<()> {
|
||||||
|
let text = serde_json::to_string(&json!([]))?;
|
||||||
|
|
||||||
|
let coverage = SourceCoverage { files: vec![] };
|
||||||
|
|
||||||
|
let ser = serde_json::to_string(&coverage)?;
|
||||||
|
assert_eq!(ser, text);
|
||||||
|
|
||||||
|
let de: SourceCoverage = serde_json::from_str(&text)?;
|
||||||
|
assert_eq!(de, coverage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_no_locations() -> Result<()> {
|
||||||
|
let text = serde_json::to_string(&json!([
|
||||||
|
{
|
||||||
|
"file": MAIN_C.to_owned(),
|
||||||
|
"locations": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": COMMON_C.to_owned(),
|
||||||
|
"locations": [],
|
||||||
|
},
|
||||||
|
]))?;
|
||||||
|
|
||||||
|
let coverage = {
|
||||||
|
let files = vec![
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: MAIN_C.to_owned(),
|
||||||
|
locations: vec![],
|
||||||
|
},
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: COMMON_C.to_owned(),
|
||||||
|
locations: vec![],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
SourceCoverage { files }
|
||||||
|
};
|
||||||
|
|
||||||
|
let ser = serde_json::to_string(&coverage)?;
|
||||||
|
assert_eq!(ser, text);
|
||||||
|
|
||||||
|
let de: SourceCoverage = serde_json::from_str(&text)?;
|
||||||
|
assert_eq!(de, coverage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_no_or_null_columns() -> Result<()> {
|
||||||
|
let text_null_cols = serde_json::to_string(&json!([
|
||||||
|
{
|
||||||
|
"file": MAIN_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 4, "column": null, "count": 1 },
|
||||||
|
{ "line": 9, "column": null, "count": 0 },
|
||||||
|
{ "line": 12, "column": null, "count": 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": COMMON_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 5, "column": null, "count": 0 },
|
||||||
|
{ "line": 5, "column": null, "count": 1 },
|
||||||
|
{ "line": 8, "column": null, "count": 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]))?;
|
||||||
|
|
||||||
|
let text_no_cols = serde_json::to_string(&json!([
|
||||||
|
{
|
||||||
|
"file": MAIN_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 4, "count": 1 },
|
||||||
|
{ "line": 9, "count": 0 },
|
||||||
|
{ "line": 12, "count": 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": COMMON_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 5, "count": 0 },
|
||||||
|
{ "line": 5, "count": 1 },
|
||||||
|
{ "line": 8, "count": 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]))?;
|
||||||
|
|
||||||
|
let coverage = {
|
||||||
|
let files = vec![
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: MAIN_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 4,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 9,
|
||||||
|
column: None,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 12,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: COMMON_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: None,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 8,
|
||||||
|
column: None,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
SourceCoverage { files }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Serialized with present `column` keys, `null` values.
|
||||||
|
let ser = serde_json::to_string(&coverage)?;
|
||||||
|
assert_eq!(ser, text_null_cols);
|
||||||
|
|
||||||
|
// Deserializes when `column` keys are absent.
|
||||||
|
let de_no_cols: SourceCoverage = serde_json::from_str(&text_no_cols)?;
|
||||||
|
assert_eq!(de_no_cols, coverage);
|
||||||
|
|
||||||
|
// Deserializes when `column` keys are present but `null`.
|
||||||
|
let de_null_cols: SourceCoverage = serde_json::from_str(&text_null_cols)?;
|
||||||
|
assert_eq!(de_null_cols, coverage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_partial_columns() -> Result<()> {
|
||||||
|
let text = serde_json::to_string(&json!([
|
||||||
|
{
|
||||||
|
"file": MAIN_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 4, "column": 4, "count": 1 },
|
||||||
|
{ "line": 9, "column": 4, "count": 0 },
|
||||||
|
{ "line": 12, "column": 4, "count": 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": COMMON_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 5, "column": null, "count": 0 },
|
||||||
|
{ "line": 5, "column": null, "count": 1 },
|
||||||
|
{ "line": 8, "column": null, "count": 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]))?;
|
||||||
|
|
||||||
|
let coverage = {
|
||||||
|
let files = vec![
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: MAIN_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 4,
|
||||||
|
column: Some(4),
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 9,
|
||||||
|
column: Some(4),
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 12,
|
||||||
|
column: Some(4),
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
SourceFileCoverage {
|
||||||
|
file: COMMON_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: None,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 5,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 8,
|
||||||
|
column: None,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
SourceCoverage { files }
|
||||||
|
};
|
||||||
|
|
||||||
|
let ser = serde_json::to_string(&coverage)?;
|
||||||
|
assert_eq!(ser, text);
|
||||||
|
|
||||||
|
let de: SourceCoverage = serde_json::from_str(&text)?;
|
||||||
|
assert_eq!(de, coverage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_coverage_mixed_columns() -> Result<()> {
|
||||||
|
let text = serde_json::to_string(&json!([
|
||||||
|
{
|
||||||
|
"file": MAIN_C.to_owned(),
|
||||||
|
"locations": [
|
||||||
|
{ "line": 4, "column": null, "count": 1 },
|
||||||
|
{ "line": 9, "column": 4, "count": 0 },
|
||||||
|
{ "line": 12, "column": null, "count": 1 },
|
||||||
|
{ "line": 13, "column": 7, "count": 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]))?;
|
||||||
|
|
||||||
|
let coverage = {
|
||||||
|
let files = vec![SourceFileCoverage {
|
||||||
|
file: MAIN_C.to_owned(),
|
||||||
|
locations: vec![
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 4,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 9,
|
||||||
|
column: Some(4),
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 12,
|
||||||
|
column: None,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
SourceCoverageLocation {
|
||||||
|
line: 13,
|
||||||
|
column: Some(7),
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
SourceCoverage { files }
|
||||||
|
};
|
||||||
|
|
||||||
|
let ser = serde_json::to_string(&coverage)?;
|
||||||
|
assert_eq!(ser, text);
|
||||||
|
|
||||||
|
let de: SourceCoverage = serde_json::from_str(&text)?;
|
||||||
|
assert_eq!(de, coverage);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user