Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Commit

Permalink
Record coverage using debuggable-module (#2701)
Browse files Browse the repository at this point in the history
  • Loading branch information
ranweiler authored Dec 15, 2022
1 parent 054910e commit ff923d2
Show file tree
Hide file tree
Showing 20 changed files with 1,410 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"atexit",
"coverage",
"coverage-legacy",
"debuggable-module",
"debugger",
Expand Down
26 changes: 26 additions & 0 deletions src/agent/coverage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "coverage"
version = "0.1.0"
edition = "2021"
license = "MIT"

[dependencies]
anyhow = "1.0"
debuggable-module = { path = "../debuggable-module" }
iced-x86 = "1.17"
log = "0.4.17"
regex = "1.0"
symbolic = { version = "10.1", features = ["debuginfo", "demangle", "symcache"] }
thiserror = "1.0"

[target.'cfg(target_os = "windows")'.dependencies]
debugger = { path = "../debugger" }

[target.'cfg(target_os = "linux")'.dependencies]
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"] }

[dev-dependencies]
clap = { version = "4.0", features = ["derive"] }
65 changes: 65 additions & 0 deletions src/agent/coverage/examples/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::process::Command;
use std::time::Duration;

use anyhow::Result;
use clap::Parser;
use coverage::allowlist::{AllowList, TargetAllowList};
use coverage::binary::BinaryCoverage;

#[derive(Parser, Debug)]
struct Args {
#[arg(long)]
module_allowlist: Option<String>,

#[arg(long)]
source_allowlist: Option<String>,

#[arg(short, long)]
timeout: Option<u64>,

command: Vec<String>,
}

const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);

fn main() -> Result<()> {
let args = Args::parse();

let timeout = args
.timeout
.map(Duration::from_millis)
.unwrap_or(DEFAULT_TIMEOUT);

let mut cmd = Command::new(&args.command[0]);
if args.command.len() > 1 {
cmd.args(&args.command[1..]);
}

let mut allowlist = TargetAllowList::default();

if let Some(path) = &args.module_allowlist {
allowlist.modules = AllowList::load(path)?;
}

if let Some(path) = &args.source_allowlist {
allowlist.source_files = AllowList::load(path)?;
}

let coverage = coverage::record::record(cmd, timeout, allowlist)?;

dump_modoff(coverage)?;

Ok(())
}

fn dump_modoff(coverage: BinaryCoverage) -> Result<()> {
for (module, coverage) in &coverage.modules {
for (offset, count) in coverage.as_ref() {
if count.reached() {
println!("{}+{offset:x}", module.base_name());
}
}
}

Ok(())
}
156 changes: 156 additions & 0 deletions src/agent/coverage/src/allowlist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use anyhow::Result;
use regex::{Regex, RegexSet};
use std::path::Path;

#[derive(Clone, Debug, Default)]
pub struct TargetAllowList {
pub functions: AllowList,
pub modules: AllowList,
pub source_files: AllowList,
}

impl TargetAllowList {
pub fn new(modules: AllowList, source_files: AllowList) -> Self {
// Allow all.
let functions = AllowList::default();

Self {
functions,
modules,
source_files,
}
}
}

#[derive(Clone, Debug)]
pub struct AllowList {
allow: RegexSet,
deny: RegexSet,
}

impl AllowList {
pub fn new(allow: RegexSet, deny: RegexSet) -> Self {
Self { allow, deny }
}

pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)?;
Self::parse(&text)
}

pub fn parse(text: &str) -> Result<Self> {
use std::io::{BufRead, BufReader};

let reader = BufReader::new(text.as_bytes());

let mut allow = vec![];
let mut deny = vec![];

// We could just collect and pass to the `RegexSet` ctor.
//
// Instead, check each rule individually for diagnostic purposes.
for (index, line) in reader.lines().enumerate() {
let line = line?;

match AllowListLine::parse(&line) {
Ok(valid) => {
use AllowListLine::*;

match valid {
Blank | Comment => {
// Ignore.
}
Allow(re) => {
allow.push(re);
}
Deny(re) => {
deny.push(re);
}
}
}
Err(err) => {
// Ignore invalid lines, but warn.
let line_number = index + 1;
warn!("error at line {}: {}", line_number, err);
}
}
}

let allow = RegexSet::new(allow.iter().map(|re| re.as_str()))?;
let deny = RegexSet::new(deny.iter().map(|re| re.as_str()))?;
let allowlist = AllowList::new(allow, deny);

Ok(allowlist)
}

pub fn is_allowed(&self, path: impl AsRef<str>) -> bool {
let path = path.as_ref();

// Allowed if rule-allowed but not excluded by a negative (deny) rule.
self.allow.is_match(path) && !self.deny.is_match(path)
}
}

impl Default for AllowList {
fn default() -> Self {
// Unwrap-safe due to valid constant expr.
let allow = RegexSet::new([".*"]).unwrap();
let deny = RegexSet::empty();

AllowList::new(allow, deny)
}
}

pub enum AllowListLine {
Blank,
Comment,
Allow(Regex),
Deny(Regex),
}

impl AllowListLine {
pub fn parse(line: &str) -> Result<Self> {
let line = line.trim();

// Allow and ignore blank lines.
if line.is_empty() {
return Ok(Self::Blank);
}

// Support comments of the form `# <comment>`.
if line.starts_with("# ") {
return Ok(Self::Comment);
}

// Deny rules are of the form `! <rule>`.
if let Some(expr) = line.strip_prefix("! ") {
let re = glob_to_regex(expr)?;
return Ok(Self::Deny(re));
}

// Try to interpret as allow rule.
let re = glob_to_regex(line)?;
Ok(Self::Allow(re))
}
}

#[allow(clippy::single_char_pattern)]
fn glob_to_regex(expr: &str) -> Result<Regex> {
// Don't make users escape Windows path separators.
let expr = expr.replace(r"\", r"\\");

// Translate glob wildcards into quantified regexes.
let expr = expr.replace("*", ".*");

// Anchor to line start and end.
let expr = format!("^{expr}$");

Ok(Regex::new(&expr)?)
}

#[cfg(test)]
mod tests;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a/*
! a/c
# c
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a/*
! a/c
c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
4 changes: 4 additions & 0 deletions src/agent/coverage/src/allowlist/test-data/allow-all.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
a
a/b
b
c
2 changes: 2 additions & 0 deletions src/agent/coverage/src/allowlist/test-data/allow-some.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a
b
Empty file.
Loading

0 comments on commit ff923d2

Please sign in to comment.