diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index 9032aff50e..b279a13224 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -519,6 +519,14 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cobertura" +version = "0.1.0" +dependencies = [ + "anyhow", + "quick-xml", +] + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -561,6 +569,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap 4.0.26", + "cobertura", "debuggable-module", "debugger", "env_logger 0.10.0", diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index e592cc2308..62b1474b20 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "atexit", + "cobertura", "coverage", "coverage-legacy", "debuggable-module", diff --git a/src/agent/cobertura/Cargo.toml b/src/agent/cobertura/Cargo.toml new file mode 100644 index 0000000000..2a6bef555f --- /dev/null +++ b/src/agent/cobertura/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cobertura" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +anyhow = "1.0" +quick-xml = "0.26.0" diff --git a/src/agent/onefuzz-file-format/src/coverage/cobertura.rs b/src/agent/cobertura/src/lib.rs similarity index 76% rename from src/agent/onefuzz-file-format/src/coverage/cobertura.rs rename to src/agent/cobertura/src/lib.rs index 6842c50c5b..9becf4a113 100644 --- a/src/agent/onefuzz-file-format/src/coverage/cobertura.rs +++ b/src/agent/cobertura/src/lib.rs @@ -1,12 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![allow(clippy::field_reassign_with_default)] -use std::collections::{BTreeMap, BTreeSet}; use std::io::{Cursor, Write}; -use coverage::source::SourceCoverage; -use debuggable_module::path::FilePath; use quick_xml::{Result, Writer}; impl CoberturaCoverage { @@ -406,87 +402,3 @@ impl WriteXml for Condition { Ok(()) } } - -// Dir -> Set -type FileMap<'a> = BTreeMap<&'a str, BTreeSet<&'a FilePath>>; - -impl From for CoberturaCoverage { - fn from(source: SourceCoverage) -> Self { - // The Cobertura data model is organized around `classes` and `methods` contained - // in `packages`. Our source coverage has no language-level assumptions. - // - // To obtain legible HTML reports using ReportGenerator, will we use `` - // elements to group files by their parent directory. Each measured source file - // will be represented a ``. The and the measured source file's lines will - // become `` elements of the (synthetic) class. - // - // Note: ReportGenerator automatically computes and rolls up aggregated coverage - // stats. We do _not_ need to manually compute any `line-rate` attributes. The - // presence of these attributes is required by the Cobertura schema, but even if - // they are set to 0 (as in our `Default` impls), ReportGenerator ignores them. - - // Source files grouped by directory. - let mut file_map = FileMap::default(); - - for file_path in source.files.keys() { - let dir = file_path.directory(); - let files = file_map.entry(dir).or_default(); - files.insert(file_path); - } - - // Iterate through the grouped files, accumulating `` elements. - let mut packages = vec![]; - let mut sources = vec![]; - - for (directory, files) in file_map { - // Make a `` to represent the directory. - // - // We will add a `` for each contained file. - let mut package = Package::default(); - package.name = directory.to_owned(); - - let mut classes = vec![]; - - for file_path in files { - // Add the file to the `` manifest element. - let src = Source { - path: file_path.to_string(), - }; - sources.push(src); - - let mut lines = vec![]; - - // Can't panic, by construction. - let file_coverage = &source.files[file_path]; - - for (line, count) in &file_coverage.lines { - let number = u64::from(line.number()); - let hits = u64::from(count.0); - - let mut line = Line::default(); - line.number = number; - line.hits = hits; - - lines.push(line); - } - - let mut class = Class::default(); - class.name = file_path.file_name().to_owned(); - class.filename = file_path.to_string(); - class.lines = Lines { lines }; - - classes.push(class); - } - - package.classes = Classes { classes }; - - packages.push(package); - } - - let mut xml = CoberturaCoverage::default(); - xml.sources = Some(Sources { sources }); - xml.packages = Packages { packages }; - - xml - } -} diff --git a/src/agent/coverage/Cargo.toml b/src/agent/coverage/Cargo.toml index 46f904e026..27f2fe61c5 100644 --- a/src/agent/coverage/Cargo.toml +++ b/src/agent/coverage/Cargo.toml @@ -6,11 +6,16 @@ license = "MIT" [dependencies] anyhow = "1.0" +cobertura = { path = "../cobertura" } debuggable-module = { path = "../debuggable-module" } iced-x86 = "1.17" log = "0.4.17" regex = "1.0" -symbolic = { version = "10.1", features = ["debuginfo", "demangle", "symcache"] } +symbolic = { version = "10.1", features = [ + "debuginfo", + "demangle", + "symcache", +] } thiserror = "1.0" [target.'cfg(target_os = "windows")'.dependencies] @@ -20,7 +25,7 @@ debugger = { path = "../debugger" } pete = "0.9" # For procfs, opt out of the `chrono` freature; it pulls in an old version # of `time`. We do not use the methods that the `chrono` feature enables. -procfs = { version = "0.12", default-features = false, features=["flate2"] } +procfs = { version = "0.12", default-features = false, features = ["flate2"] } [dev-dependencies] clap = { version = "4.0", features = ["derive"] } diff --git a/src/agent/onefuzz-file-format/examples/cobertura.rs b/src/agent/coverage/examples/cobertura.rs similarity index 94% rename from src/agent/onefuzz-file-format/examples/cobertura.rs rename to src/agent/coverage/examples/cobertura.rs index 2ed70e405c..895567a841 100644 --- a/src/agent/onefuzz-file-format/examples/cobertura.rs +++ b/src/agent/coverage/examples/cobertura.rs @@ -3,9 +3,9 @@ use anyhow::Result; +use cobertura::CoberturaCoverage; use coverage::source::{Count, FileCoverage, Line, SourceCoverage}; use debuggable_module::path::FilePath; -use onefuzz_file_format::coverage::cobertura::CoberturaCoverage; fn main() -> Result<()> { let modoff = vec![ diff --git a/src/agent/coverage/examples/coverage.rs b/src/agent/coverage/examples/coverage.rs index 45e53daaff..def37267d8 100644 --- a/src/agent/coverage/examples/coverage.rs +++ b/src/agent/coverage/examples/coverage.rs @@ -3,6 +3,7 @@ use std::time::Duration; use anyhow::Result; use clap::Parser; +use cobertura::CoberturaCoverage; use coverage::allowlist::{AllowList, TargetAllowList}; use coverage::binary::BinaryCoverage; use coverage::record::CoverageRecorder; @@ -19,8 +20,8 @@ struct Args { #[arg(short, long)] timeout: Option, - #[arg(short, long)] - source: bool, + #[arg(short, long, value_enum, default_value_t = OutputFormat::ModOff)] + output: OutputFormat, #[arg(long)] dump_stdio: bool, @@ -28,6 +29,13 @@ struct Args { command: Vec, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum)] +enum OutputFormat { + ModOff, + Source, + Cobertura, +} + const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); fn main() -> Result<()> { @@ -79,10 +87,10 @@ fn main() -> Result<()> { println!(); } - if args.source { - dump_source_line(&recorded.coverage)?; - } else { - dump_modoff(&recorded.coverage)?; + match args.output { + OutputFormat::ModOff => dump_modoff(&recorded.coverage)?, + OutputFormat::Source => dump_source_line(&recorded.coverage)?, + OutputFormat::Cobertura => dump_cobertura(&recorded.coverage)?, } Ok(()) @@ -111,3 +119,12 @@ fn dump_source_line(binary: &BinaryCoverage) -> Result<()> { Ok(()) } + +fn dump_cobertura(binary: &BinaryCoverage) -> Result<()> { + let source = coverage::source::binary_to_source_coverage(binary)?; + let cobertura: CoberturaCoverage = source.into(); + + println!("{}", cobertura.to_string()?); + + Ok(()) +} diff --git a/src/agent/coverage/src/cobertura.rs b/src/agent/coverage/src/cobertura.rs new file mode 100644 index 0000000000..2d66ca3c88 --- /dev/null +++ b/src/agent/coverage/src/cobertura.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::{BTreeMap, BTreeSet}; + +use cobertura::{ + Class, Classes, CoberturaCoverage, Line, Lines, Package, Packages, Source, Sources, +}; +use debuggable_module::path::FilePath; + +use crate::source::SourceCoverage; + +// Dir -> Set +type FileMap<'a> = BTreeMap<&'a str, BTreeSet<&'a FilePath>>; + +impl From for CoberturaCoverage { + fn from(source: SourceCoverage) -> Self { + // The Cobertura data model is organized around `classes` and `methods` contained + // in `packages`. Our source coverage has no language-level assumptions. + // + // To obtain legible HTML reports using ReportGenerator, will we use `` + // elements to group files by their parent directory. Each measured source file + // will be represented a ``. The and the measured source file's lines will + // become `` elements of the (synthetic) class. + // + // Note: ReportGenerator automatically computes and rolls up aggregated coverage + // stats. We do _not_ need to manually compute any `line-rate` attributes. The + // presence of these attributes is required by the Cobertura schema, but even if + // they are set to 0 (as in our `Default` impls), ReportGenerator ignores them. + + // Source files grouped by directory. + let mut file_map = FileMap::default(); + + for file_path in source.files.keys() { + let dir = file_path.directory(); + let files = file_map.entry(dir).or_default(); + files.insert(file_path); + } + + // Iterate through the grouped files, accumulating `` elements. + let mut packages = vec![]; + let mut sources = vec![]; + + for (directory, files) in file_map { + // Make a `` to represent the directory. + // + // We will add a `` for each contained file. + let mut package = Package { + name: directory.to_owned(), + ..Package::default() + }; + + let mut classes = vec![]; + + for file_path in files { + // Add the file to the `` manifest element. + let src = Source { + path: file_path.to_string(), + }; + sources.push(src); + + let mut lines = vec![]; + + // Can't panic, by construction. + let file_coverage = &source.files[file_path]; + + for (line, count) in &file_coverage.lines { + let number = u64::from(line.number()); + let hits = u64::from(count.0); + + let line = Line { + number, + hits, + ..Line::default() + }; + + lines.push(line); + } + + let class = Class { + name: file_path.file_name().to_owned(), + filename: file_path.to_string(), + lines: Lines { lines }, + ..Class::default() + }; + + classes.push(class); + } + + package.classes = Classes { classes }; + + packages.push(package); + } + + CoberturaCoverage { + sources: Some(Sources { sources }), + packages: Packages { packages }, + ..CoberturaCoverage::default() + } + } +} diff --git a/src/agent/coverage/src/lib.rs b/src/agent/coverage/src/lib.rs index bb88bb3534..499e82a924 100644 --- a/src/agent/coverage/src/lib.rs +++ b/src/agent/coverage/src/lib.rs @@ -6,6 +6,7 @@ extern crate log; pub mod allowlist; pub mod binary; +pub mod cobertura; pub mod record; pub mod source; mod timer; diff --git a/src/agent/onefuzz-file-format/src/coverage.rs b/src/agent/onefuzz-file-format/src/coverage.rs index f83f2dc192..34e921cac5 100644 --- a/src/agent/onefuzz-file-format/src/coverage.rs +++ b/src/agent/onefuzz-file-format/src/coverage.rs @@ -2,5 +2,4 @@ // Licensed under the MIT License. pub mod binary; -pub mod cobertura; pub mod source;