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

Commit ff923d2

Browse files
authored
Record coverage using debuggable-module (#2701)
1 parent 054910e commit ff923d2

File tree

20 files changed

+1410
-0
lines changed

20 files changed

+1410
-0
lines changed

src/agent/Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/agent/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"atexit",
4+
"coverage",
45
"coverage-legacy",
56
"debuggable-module",
67
"debugger",

src/agent/coverage/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "coverage"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT"
6+
7+
[dependencies]
8+
anyhow = "1.0"
9+
debuggable-module = { path = "../debuggable-module" }
10+
iced-x86 = "1.17"
11+
log = "0.4.17"
12+
regex = "1.0"
13+
symbolic = { version = "10.1", features = ["debuginfo", "demangle", "symcache"] }
14+
thiserror = "1.0"
15+
16+
[target.'cfg(target_os = "windows")'.dependencies]
17+
debugger = { path = "../debugger" }
18+
19+
[target.'cfg(target_os = "linux")'.dependencies]
20+
pete = "0.9"
21+
# For procfs, opt out of the `chrono` freature; it pulls in an old version
22+
# of `time`. We do not use the methods that the `chrono` feature enables.
23+
procfs = { version = "0.12", default-features = false, features=["flate2"] }
24+
25+
[dev-dependencies]
26+
clap = { version = "4.0", features = ["derive"] }
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use std::process::Command;
2+
use std::time::Duration;
3+
4+
use anyhow::Result;
5+
use clap::Parser;
6+
use coverage::allowlist::{AllowList, TargetAllowList};
7+
use coverage::binary::BinaryCoverage;
8+
9+
#[derive(Parser, Debug)]
10+
struct Args {
11+
#[arg(long)]
12+
module_allowlist: Option<String>,
13+
14+
#[arg(long)]
15+
source_allowlist: Option<String>,
16+
17+
#[arg(short, long)]
18+
timeout: Option<u64>,
19+
20+
command: Vec<String>,
21+
}
22+
23+
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
24+
25+
fn main() -> Result<()> {
26+
let args = Args::parse();
27+
28+
let timeout = args
29+
.timeout
30+
.map(Duration::from_millis)
31+
.unwrap_or(DEFAULT_TIMEOUT);
32+
33+
let mut cmd = Command::new(&args.command[0]);
34+
if args.command.len() > 1 {
35+
cmd.args(&args.command[1..]);
36+
}
37+
38+
let mut allowlist = TargetAllowList::default();
39+
40+
if let Some(path) = &args.module_allowlist {
41+
allowlist.modules = AllowList::load(path)?;
42+
}
43+
44+
if let Some(path) = &args.source_allowlist {
45+
allowlist.source_files = AllowList::load(path)?;
46+
}
47+
48+
let coverage = coverage::record::record(cmd, timeout, allowlist)?;
49+
50+
dump_modoff(coverage)?;
51+
52+
Ok(())
53+
}
54+
55+
fn dump_modoff(coverage: BinaryCoverage) -> Result<()> {
56+
for (module, coverage) in &coverage.modules {
57+
for (offset, count) in coverage.as_ref() {
58+
if count.reached() {
59+
println!("{}+{offset:x}", module.base_name());
60+
}
61+
}
62+
}
63+
64+
Ok(())
65+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use anyhow::Result;
5+
use regex::{Regex, RegexSet};
6+
use std::path::Path;
7+
8+
#[derive(Clone, Debug, Default)]
9+
pub struct TargetAllowList {
10+
pub functions: AllowList,
11+
pub modules: AllowList,
12+
pub source_files: AllowList,
13+
}
14+
15+
impl TargetAllowList {
16+
pub fn new(modules: AllowList, source_files: AllowList) -> Self {
17+
// Allow all.
18+
let functions = AllowList::default();
19+
20+
Self {
21+
functions,
22+
modules,
23+
source_files,
24+
}
25+
}
26+
}
27+
28+
#[derive(Clone, Debug)]
29+
pub struct AllowList {
30+
allow: RegexSet,
31+
deny: RegexSet,
32+
}
33+
34+
impl AllowList {
35+
pub fn new(allow: RegexSet, deny: RegexSet) -> Self {
36+
Self { allow, deny }
37+
}
38+
39+
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
40+
let path = path.as_ref();
41+
let text = std::fs::read_to_string(path)?;
42+
Self::parse(&text)
43+
}
44+
45+
pub fn parse(text: &str) -> Result<Self> {
46+
use std::io::{BufRead, BufReader};
47+
48+
let reader = BufReader::new(text.as_bytes());
49+
50+
let mut allow = vec![];
51+
let mut deny = vec![];
52+
53+
// We could just collect and pass to the `RegexSet` ctor.
54+
//
55+
// Instead, check each rule individually for diagnostic purposes.
56+
for (index, line) in reader.lines().enumerate() {
57+
let line = line?;
58+
59+
match AllowListLine::parse(&line) {
60+
Ok(valid) => {
61+
use AllowListLine::*;
62+
63+
match valid {
64+
Blank | Comment => {
65+
// Ignore.
66+
}
67+
Allow(re) => {
68+
allow.push(re);
69+
}
70+
Deny(re) => {
71+
deny.push(re);
72+
}
73+
}
74+
}
75+
Err(err) => {
76+
// Ignore invalid lines, but warn.
77+
let line_number = index + 1;
78+
warn!("error at line {}: {}", line_number, err);
79+
}
80+
}
81+
}
82+
83+
let allow = RegexSet::new(allow.iter().map(|re| re.as_str()))?;
84+
let deny = RegexSet::new(deny.iter().map(|re| re.as_str()))?;
85+
let allowlist = AllowList::new(allow, deny);
86+
87+
Ok(allowlist)
88+
}
89+
90+
pub fn is_allowed(&self, path: impl AsRef<str>) -> bool {
91+
let path = path.as_ref();
92+
93+
// Allowed if rule-allowed but not excluded by a negative (deny) rule.
94+
self.allow.is_match(path) && !self.deny.is_match(path)
95+
}
96+
}
97+
98+
impl Default for AllowList {
99+
fn default() -> Self {
100+
// Unwrap-safe due to valid constant expr.
101+
let allow = RegexSet::new([".*"]).unwrap();
102+
let deny = RegexSet::empty();
103+
104+
AllowList::new(allow, deny)
105+
}
106+
}
107+
108+
pub enum AllowListLine {
109+
Blank,
110+
Comment,
111+
Allow(Regex),
112+
Deny(Regex),
113+
}
114+
115+
impl AllowListLine {
116+
pub fn parse(line: &str) -> Result<Self> {
117+
let line = line.trim();
118+
119+
// Allow and ignore blank lines.
120+
if line.is_empty() {
121+
return Ok(Self::Blank);
122+
}
123+
124+
// Support comments of the form `# <comment>`.
125+
if line.starts_with("# ") {
126+
return Ok(Self::Comment);
127+
}
128+
129+
// Deny rules are of the form `! <rule>`.
130+
if let Some(expr) = line.strip_prefix("! ") {
131+
let re = glob_to_regex(expr)?;
132+
return Ok(Self::Deny(re));
133+
}
134+
135+
// Try to interpret as allow rule.
136+
let re = glob_to_regex(line)?;
137+
Ok(Self::Allow(re))
138+
}
139+
}
140+
141+
#[allow(clippy::single_char_pattern)]
142+
fn glob_to_regex(expr: &str) -> Result<Regex> {
143+
// Don't make users escape Windows path separators.
144+
let expr = expr.replace(r"\", r"\\");
145+
146+
// Translate glob wildcards into quantified regexes.
147+
let expr = expr.replace("*", ".*");
148+
149+
// Anchor to line start and end.
150+
let expr = format!("^{expr}$");
151+
152+
Ok(Regex::new(&expr)?)
153+
}
154+
155+
#[cfg(test)]
156+
mod tests;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a/*
2+
! a/c
3+
# c
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a/*
2+
! a/c
3+
c
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
a
2+
a/b
3+
b
4+
c
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
a
2+
b

0 commit comments

Comments
 (0)