From 07c8217675b05804a1aa32a92f7a14a93c8dbe37 Mon Sep 17 00:00:00 2001 From: Joe Ranweiler Date: Thu, 28 Oct 2021 14:13:33 -0700 Subject: [PATCH] Add source coverage file format (#1403) --- src/agent/coverage/src/lib.rs | 1 + src/agent/coverage/src/source.rs | 445 +++++++++++++++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 src/agent/coverage/src/source.rs diff --git a/src/agent/coverage/src/lib.rs b/src/agent/coverage/src/lib.rs index 2be459ab4..1644bae5d 100644 --- a/src/agent/coverage/src/lib.rs +++ b/src/agent/coverage/src/lib.rs @@ -21,6 +21,7 @@ pub mod code; pub mod demangle; pub mod report; pub mod sancov; +pub mod source; #[cfg(target_os = "linux")] pub mod disasm; diff --git a/src/agent/coverage/src/source.rs b/src/agent/coverage/src/source.rs new file mode 100644 index 000000000..cf2530f29 --- /dev/null +++ b/src/agent/coverage/src/source.rs @@ -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, +} + +#[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, +} + +#[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, + + /// Execution count at location. + pub count: u32, +} + +impl SourceCoverageLocation { + pub fn new(line: u32, column: impl Into>, count: u32) -> Result { + 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(()) + } +}