Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(unstable): deno test --coverage #6901

Merged
merged 69 commits into from
Sep 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
33431d6
feat(cli): add cover option to the test command
caspervonb Jul 24, 2020
0a3607a
run tools/format.py
caspervonb Jul 27, 2020
79e2ffa
Lint
caspervonb Jul 27, 2020
c8e4906
Write filtered script coverage to stdout as json
caspervonb Jul 28, 2020
292afc6
Request call count from precise coverage
caspervonb Jul 28, 2020
15c592a
s/cover/coverage/
caspervonb Jul 28, 2020
e71444f
Make --coverage require --inspect in clap
caspervonb Jul 28, 2020
961fcd2
Run tools/format.py
caspervonb Jul 28, 2020
7032169
Print a basic line coverage report
caspervonb Jul 29, 2020
03aaa9d
Fix lint
caspervonb Jul 29, 2020
05b47d7
Remove redundant wait for session
caspervonb Jul 31, 2020
18561cc
Require unstable flag
caspervonb Aug 26, 2020
fd9c303
Merge branch 'master' into feat-test-coverage
caspervonb Aug 26, 2020
e9a1b0d
lint
bartlomieju Aug 26, 2020
c2fa6ed
Use file_fetcher to fetch sources
caspervonb Sep 3, 2020
9d1eec8
Add a basic integration test
caspervonb Sep 3, 2020
2f185d3
Ignore non-fetchable files
caspervonb Sep 3, 2020
9556a62
Move the line report into a PrettyCoverageReporter struct
caspervonb Sep 3, 2020
676dc45
Format
caspervonb Sep 3, 2020
5d0bfee
Add source maps, but don't actually do anything useful with them
caspervonb Sep 3, 2020
2d49f1b
Merge branch 'master' into feat-test-coverage
caspervonb Sep 4, 2020
73d89b7
Format
caspervonb Sep 4, 2020
48e9545
Remove debugging println!
caspervonb Sep 4, 2020
17e2498
Use compiled source instead source map
caspervonb Sep 4, 2020
16e0448
Format
caspervonb Sep 4, 2020
6ce9841
Lint
caspervonb Sep 4, 2020
ea51b6a
Format
caspervonb Sep 4, 2020
48a98e9
Rename some locals
caspervonb Sep 4, 2020
79a7bbc
Colorize output
caspervonb Sep 4, 2020
90a387b
Ignore non resolvable sources
caspervonb Sep 4, 2020
d21d51c
Remove the ignore condition, not needed
caspervonb Sep 4, 2020
2b9d491
Format
caspervonb Sep 4, 2020
c1f4b41
Tally up counts
caspervonb Sep 5, 2020
a997233
Format
caspervonb Sep 5, 2020
f8bd921
Filter to parent directories of test scripts
caspervonb Sep 5, 2020
828e2de
Use yellow for warning range
caspervonb Sep 5, 2020
636fe6b
Filter out test_file_url
caspervonb Sep 5, 2020
22aa294
Merge branch 'master' into feat-test-coverage
caspervonb Sep 5, 2020
a40e3fa
Use tokio-tungstenite version 0.11.0
caspervonb Sep 5, 2020
0791926
Bump CI
caspervonb Sep 5, 2020
7b7e98b
Merge branch 'master' into feat-test-coverage
caspervonb Sep 5, 2020
0976dfc
Fix merge
caspervonb Sep 5, 2020
3114384
Yield to worker before trying to connect
caspervonb Sep 6, 2020
e27161f
Delay until the inspector server is listening
caspervonb Sep 6, 2020
efee8f0
Wait even longer, because of WSAStartup
caspervonb Sep 6, 2020
8cd1232
5 Second delay for CI
caspervonb Sep 6, 2020
3e64403
Move away from websocket
caspervonb Sep 6, 2020
2c0bf16
Make it work with channel/session pair
caspervonb Sep 6, 2020
e49882b
Remove 'debugger session started' from test
caspervonb Sep 6, 2020
3acb419
Lint
caspervonb Sep 6, 2020
929dfee
Oh god why
caspervonb Sep 6, 2020
8cdeee3
Attempting again
caspervonb Sep 6, 2020
052ce95
Remove debug delay
caspervonb Sep 6, 2020
22f4473
Clean up dispatch calls
caspervonb Sep 7, 2020
b8a3b1e
Clean up attempt at fixing winsock error
caspervonb Sep 7, 2020
d46b72e
Format
caspervonb Sep 7, 2020
5a060e0
Use a plain queue
caspervonb Sep 8, 2020
70ba478
Merge remote-tracking branch 'origin/master' into feat-test-coverage
piscisaureus Sep 9, 2020
9153167
fix(cli): suppress 'WSANOTINITIALIZED' error on Deno exit
piscisaureus Sep 9, 2020
b8f9b5a
fail-fast off
piscisaureus Sep 9, 2020
5858ece
Allow for race between Check ... and Debugger listening message
caspervonb Sep 9, 2020
59496f3
Revert "fail-fast off"
caspervonb Sep 9, 2020
4ecd1b1
Merge branch 'master' into feat-test-coverage
bartlomieju Sep 10, 2020
6f9eed5
imply using --inspect
bartlomieju Sep 11, 2020
1b93374
cleanup
bartlomieju Sep 11, 2020
cf07489
reset CI
bartlomieju Sep 11, 2020
0b5b632
Merge branch 'master' into feat-test-coverage
bartlomieju Sep 11, 2020
11261fc
Merge branch 'master' into feat-test-coverage
bartlomieju Sep 13, 2020
af11ce9
Merge branch 'master' into feat-test-coverage
bartlomieju Sep 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions cli/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.

use crate::colors;
use crate::file_fetcher::SourceFile;
use crate::global_state::GlobalState;
use crate::inspector::DenoInspector;
use crate::permissions::Permissions;
use deno_core::v8;
use deno_core::ErrBox;
use deno_core::ModuleSpecifier;
use serde::Deserialize;
use std::collections::VecDeque;
use std::mem::MaybeUninit;
use std::ops::Deref;
use std::ops::DerefMut;
use std::ptr;
use std::sync::Arc;
use url::Url;

pub struct CoverageCollector {
v8_channel: v8::inspector::ChannelBase,
v8_session: v8::UniqueRef<v8::inspector::V8InspectorSession>,
response_queue: VecDeque<String>,
}

impl Deref for CoverageCollector {
type Target = v8::inspector::V8InspectorSession;
fn deref(&self) -> &Self::Target {
&self.v8_session
}
}

impl DerefMut for CoverageCollector {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.v8_session
}
}

impl v8::inspector::ChannelImpl for CoverageCollector {
fn base(&self) -> &v8::inspector::ChannelBase {
&self.v8_channel
}

fn base_mut(&mut self) -> &mut v8::inspector::ChannelBase {
&mut self.v8_channel
}

fn send_response(
&mut self,
_call_id: i32,
message: v8::UniquePtr<v8::inspector::StringBuffer>,
) {
let message = message.unwrap().string().to_string();
self.response_queue.push_back(message);
}

fn send_notification(
&mut self,
_message: v8::UniquePtr<v8::inspector::StringBuffer>,
) {
}

fn flush_protocol_notifications(&mut self) {}
}

impl CoverageCollector {
const CONTEXT_GROUP_ID: i32 = 1;

pub fn new(inspector_ptr: *mut DenoInspector) -> Box<Self> {
new_box_with(move |self_ptr| {
let v8_channel = v8::inspector::ChannelBase::new::<Self>();
let v8_session = unsafe { &mut *inspector_ptr }.connect(
Self::CONTEXT_GROUP_ID,
unsafe { &mut *self_ptr },
v8::inspector::StringView::empty(),
);

let response_queue = VecDeque::with_capacity(10);

Self {
v8_channel,
v8_session,
response_queue,
}
})
}

async fn dispatch(&mut self, message: String) -> Result<String, ErrBox> {
let message = v8::inspector::StringView::from(message.as_bytes());
self.v8_session.dispatch_protocol_message(message);

let response = self.response_queue.pop_back();
Ok(response.unwrap())
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
}

pub async fn start_collecting(&mut self) -> Result<(), ErrBox> {
self
.dispatch(r#"{"id":1,"method":"Runtime.enable"}"#.into())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increment a message iterator, like self.next_message_id.

.await?;
self
.dispatch(r#"{"id":2,"method":"Profiler.enable"}"#.into())
.await?;

self
.dispatch(r#"{"id":3,"method":"Profiler.startPreciseCoverage", "params": {"callCount": true, "detailed": true}}"#.into())
.await?;

Ok(())
}

pub async fn take_precise_coverage(
&mut self,
) -> Result<Vec<ScriptCoverage>, ErrBox> {
let response = self
.dispatch(r#"{"id":4,"method":"Profiler.takePreciseCoverage" }"#.into())
.await?;

let coverage_result: TakePreciseCoverageResponse =
serde_json::from_str(&response).unwrap();

Ok(coverage_result.result.result)
}

pub async fn stop_collecting(&mut self) -> Result<(), ErrBox> {
self
.dispatch(r#"{"id":5,"method":"Profiler.stopPreciseCoverage"}"#.into())
.await?;

self
.dispatch(r#"{"id":6,"method":"Profiler.disable"}"#.into())
.await?;

self
.dispatch(r#"{"id":7,"method":"Runtime.disable"}"#.into())
.await?;

Ok(())
}
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CoverageRange {
pub start_offset: usize,
pub end_offset: usize,
pub count: usize,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FunctionCoverage {
pub function_name: String,
pub ranges: Vec<CoverageRange>,
pub is_block_coverage: bool,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScriptCoverage {
pub script_id: String,
pub url: String,
pub functions: Vec<FunctionCoverage>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TakePreciseCoverageResult {
result: Vec<ScriptCoverage>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TakePreciseCoverageResponse {
id: usize,
result: TakePreciseCoverageResult,
}

pub struct PrettyCoverageReporter {
coverages: Vec<ScriptCoverage>,
global_state: Arc<GlobalState>,
}

// TODO(caspervonb) add support for lcov output (see geninfo(1) for format spec).
impl PrettyCoverageReporter {
pub fn new(
global_state: Arc<GlobalState>,
coverages: Vec<ScriptCoverage>,
) -> PrettyCoverageReporter {
PrettyCoverageReporter {
global_state,
coverages,
}
}

pub fn get_report(&self) -> String {
let mut report = String::from("test coverage:\n");

for script_coverage in &self.coverages {
if let Some(script_report) = self.get_script_report(script_coverage) {
report.push_str(&format!("{}\n", script_report))
}
}

report
}

fn get_source_file_for_script(
&self,
script_coverage: &ScriptCoverage,
) -> Option<SourceFile> {
let module_specifier =
ModuleSpecifier::resolve_url_or_path(&script_coverage.url).ok()?;

let maybe_source_file = self
.global_state
.ts_compiler
.get_compiled_source_file(&module_specifier.as_url())
.or_else(|_| {
self
.global_state
.file_fetcher
.fetch_cached_source_file(&module_specifier, Permissions::allow_all())
.ok_or_else(|| ErrBox::error("unable to fetch source file"))
})
.ok();

maybe_source_file
}

fn get_script_report(
&self,
script_coverage: &ScriptCoverage,
) -> Option<String> {
let source_file = match self.get_source_file_for_script(script_coverage) {
Some(sf) => sf,
None => return None,
};

let mut total_lines = 0;
let mut covered_lines = 0;

let mut line_offset = 0;
let source_string = source_file.source_code.to_string().unwrap();

for line in source_string.lines() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a core/std line iterator that can us these line offsets?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so

let line_start_offset = line_offset;
let line_end_offset = line_start_offset + line.len();

let mut count = 0;
for function in &script_coverage.functions {
for range in &function.ranges {
if range.start_offset <= line_start_offset
&& range.end_offset >= line_end_offset
{
count += range.count;
if range.count == 0 {
count = 0;
break;
}
}
}
}

if count > 0 {
covered_lines += 1;
}

total_lines += 1;
line_offset += line.len();
}

let line_ratio = covered_lines as f32 / total_lines as f32;
let line_coverage = format!("{:.3}%", line_ratio * 100.0);

let line = if line_ratio >= 0.9 {
format!(
"{} {}",
source_file.url.to_string(),
colors::green(&line_coverage)
)
} else if line_ratio >= 0.75 {
format!(
"{} {}",
source_file.url.to_string(),
colors::yellow(&line_coverage)
)
} else {
format!(
"{} {}",
source_file.url.to_string(),
colors::red(&line_coverage)
)
};

Some(line)
}
}

fn new_box_with<T>(new_fn: impl FnOnce(*mut T) -> T) -> Box<T> {
let b = Box::new(MaybeUninit::<T>::uninit());
let p = Box::into_raw(b) as *mut T;
unsafe { ptr::write(p, new_fn(p)) };
unsafe { Box::from_raw(p) }
}

pub fn filter_script_coverages(
coverages: Vec<ScriptCoverage>,
test_file_url: Url,
test_modules: Vec<Url>,
) -> Vec<ScriptCoverage> {
coverages
.into_iter()
.filter(|e| {
if let Ok(url) = Url::parse(&e.url) {
if url == test_file_url {
return false;
}

for test_module_url in &test_modules {
if &url == test_module_url {
return false;
}
}

if let Ok(path) = url.to_file_path() {
for test_module_url in &test_modules {
if let Ok(test_module_path) = test_module_url.to_file_path() {
if path.starts_with(test_module_path.parent().unwrap()) {
return true;
}
}
}
}
}

false
})
.collect::<Vec<ScriptCoverage>>()
}
Loading