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

[WIP] test coverage #4802

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ walkdir = "2.3.1"
warp = "0.2.2"
semver-parser = "0.9.0"
uuid = { version = "0.8.1", features = ["v4"] }
tokio-tungstenite = { version = "0.10.1", features = ["connect"] }

[target.'cfg(windows)'.dependencies]
winapi = "0.3.8"
Expand All @@ -75,7 +76,7 @@ nix = "0.17.0"
[dev-dependencies]
os_pipe = "0.9.1"
# Used for testing inspector. Keep in-sync with warp.
tokio-tungstenite = { version = "0.10.1", features = ["connect"] }
# tokio-tungstenite = { version = "0.10.1", features = ["connect"] }

[target.'cfg(unix)'.dev-dependencies]
pty = "0.2.2"
130 changes: 130 additions & 0 deletions cli/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.

#![allow(unused)]

use crate::futures::SinkExt;
use crate::futures::StreamExt;
use crate::tokio_util;
use deno_core::ErrBox;
use serde::Deserialize;
use std;
use tokio_tungstenite;
use url::Url;

pub struct CoverageCollector {
msg_id: usize,
socket: tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
}

impl CoverageCollector {
pub async fn connect(url: Url) -> Result<Self, ErrBox> {
let (socket, response) = tokio_tungstenite::connect_async(url)
.await
.expect("Can't connect");
assert_eq!(response.status(), 101);

let mut collector = Self { msg_id: 1, socket };

eprintln!("start");
collector
.socket
.send(r#"{"id":1,"method":"Runtime.enable"}"#.into())
.await
.unwrap();
collector
.socket
.send(r#"{"id":2,"method":"Profiler.enable"}"#.into())
.await
.unwrap();
collector.socket.send(r#"{"id":3,"method":"Profiler.startPreciseCoverage", "params": {"callCount": false, "detailed": true } }"#.into()).await.unwrap();
collector
.socket
.send(r#"{"id":4,"method":"Runtime.runIfWaitingForDebugger" }"#.into())
.await
.unwrap();
eprintln!("start1");

Ok(collector)
}

pub async fn stop_collecting(&mut self) -> Result<(), ErrBox> {
let msg = self.socket.next().await.unwrap();
dbg!(msg);
let msg = self.socket.next().await.unwrap();
eprintln!("start2");
dbg!(msg);
let msg = self.socket.next().await.unwrap();
dbg!(msg);
let msg = self.socket.next().await.unwrap();
dbg!(msg);
let msg = self.socket.next().await.unwrap();
dbg!(msg);

self
.socket
.send(r#"{"id":5,"method":"Profiler.takePreciseCoverage" }"#.into())
.await
.unwrap();
self
.socket
.send(r#"{"id":6,"method":"Profiler.stopPreciseCoverage" }"#.into())
.await
.unwrap();
Ok(())
}

pub async fn get_report(&mut self) -> Result<Vec<CoverageResult>, ErrBox> {
dbg!("before recv");
let msg = self.socket.next().await.unwrap();
dbg!("after recv");
let msg = msg.unwrap();
let msg_text = msg.to_text()?;

let coverage_result: CoverageResultMsg =
serde_json::from_str(msg_text).unwrap();
// eprintln!("cover result {:#?}", coverage_result);

// dbg!(msg);
let msg = self.socket.next().await.unwrap();
dbg!(msg);

Ok(coverage_result.result.result)
}
}

#[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 FunctionCoverageResult {
pub function_name: String,
pub ranges: Vec<CoverageRange>,
pub is_block_coverage: bool,
}

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

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

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CoverageResultMsg {
id: usize,
result: Res,
}
10 changes: 10 additions & 0 deletions cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub enum DenoSubcommand {
allow_none: bool,
include: Option<Vec<String>>,
filter: Option<String>,
coverage: bool,
},
Types,
Upgrade {
Expand Down Expand Up @@ -541,6 +542,7 @@ fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) {

let failfast = matches.is_present("failfast");
let allow_none = matches.is_present("allow_none");
let coverage = matches.is_present("coverage");
let filter = matches.value_of("filter").map(String::from);
let include = if matches.is_present("files") {
let files: Vec<String> = matches
Expand All @@ -558,6 +560,7 @@ fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
include,
filter,
allow_none,
coverage,
};
}

Expand Down Expand Up @@ -975,6 +978,11 @@ fn test_subcommand<'a, 'b>() -> App<'a, 'b> {
.takes_value(true)
.help("A pattern to filter the tests to run by"),
)
.arg(
Arg::with_name("coverage")
.long("coverage")
.help("Collect coverage information, requires --inspect flag"),
)
.arg(
Arg::with_name("files")
.help("List of file names to run")
Expand Down Expand Up @@ -2303,6 +2311,7 @@ mod tests {
filter: None,
allow_none: true,
include: Some(svec!["dir1/", "dir2/"]),
coverage: false,
},
allow_read: true,
allow_net: true,
Expand All @@ -2322,6 +2331,7 @@ mod tests {
allow_none: false,
filter: Some("foo".to_string()),
include: Some(svec!["dir1"]),
coverage: false,
},
allow_read: true,
..Flags::default()
Expand Down
71 changes: 66 additions & 5 deletions cli/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extern crate url;
mod checksum;
pub mod colors;
pub mod compilers;
mod coverage;
pub mod deno_dir;
pub mod diagnostics;
mod disk_cache;
Expand Down Expand Up @@ -66,6 +67,8 @@ pub use dprint_plugin_typescript::swc_ecma_ast;
pub use dprint_plugin_typescript::swc_ecma_parser;

use crate::compilers::TargetLib;
use crate::coverage::CoverageCollector;
use crate::coverage::CoverageResult;
use crate::doc::parser::DocFileLoader;
use crate::file_fetcher::SourceFile;
use crate::file_fetcher::SourceFileFetcher;
Expand Down Expand Up @@ -482,6 +485,7 @@ async fn test_command(
fail_fast: bool,
allow_none: bool,
filter: Option<String>,
coverage: bool,
) -> Result<(), ErrBox> {
let global_state = GlobalState::new(flags.clone())?;
let cwd = std::env::current_dir().expect("No current directory");
Expand All @@ -500,11 +504,18 @@ async fn test_command(
let test_file_url =
Url::from_file_path(&test_file_path).expect("Should be valid file url");
let test_file =
test_runner::render_test_file(test_modules, fail_fast, filter);
test_runner::render_test_file(test_modules.clone(), fail_fast, filter);
let main_module =
ModuleSpecifier::resolve_url(&test_file_url.to_string()).unwrap();

// TODO(bartlomieju):
// * for --coverage create new `tokio_tungstenite` connection here,
// probably on separate thread
// * ensure that --inspect flag is on so inspector is available

let mut worker =
create_main_worker(global_state.clone(), main_module.clone())?;

// Create a dummy source file.
let source_file = SourceFile {
filename: test_file_url.to_file_path().unwrap(),
Expand All @@ -521,11 +532,61 @@ async fn test_command(
.global_state
.file_fetcher
.save_source_file_in_cache(&main_module, source_file);

// TODO: * start collecting coverage
// coverage_collector.start_collecting();
let mut maybe_coverage_collector = if coverage {
let deno_inspector = match worker.inspector.as_ref() {
Some(inspector) => inspector,
None => {
return Err(
OpError::other("coverage option requires --inspect flag".to_string())
.into(),
)
}
};
let inspector_url = Url::parse(&deno_inspector.debugger_url)?;
Some(CoverageCollector::connect(inspector_url).await?)
} else {
None
};

let execute_result = worker.execute_module(&main_module).await;
execute_result?;
worker.execute("window.dispatchEvent(new Event('load'))")?;
(&mut *worker).await?;
worker.execute("window.dispatchEvent(new Event('unload'))")
worker.execute("window.dispatchEvent(new Event('unload'))")?;

// TODO(bartlomieju):
// * stop collecting collecting coverage
// * wait for inspector client thread to join and return
// coverage JSON struct
// * parse coverage report to CoverageParser together with list
// of test files and prepare a test/JSON report
// coverage_collector.stop_collecting();

if let Some(coverage_collector) = maybe_coverage_collector.as_mut() {
coverage_collector.stop_collecting().await?;
eprintln!("stop collecting");
(&mut *worker).await?;
(&mut *worker).await?;
(&mut *worker).await?;
(&mut *worker).await?;
eprintln!("polled worker");
let coverage_report = coverage_collector.get_report().await?;
let test_modules = test_modules
.into_iter()
.map(|u| u.to_string())
.collect::<Vec<String>>();
let filtered_report = coverage_report
.into_iter()
.filter(|e| test_modules.contains(&e.url))
.collect::<Vec<CoverageResult>>();
eprintln!("test modules {:#?}", test_modules);
eprintln!("coverage report: {:#?}", filtered_report);
}

Ok(())
}

pub fn main() {
Expand Down Expand Up @@ -584,9 +645,9 @@ pub fn main() {
include,
allow_none,
filter,
} => {
test_command(flags, include, fail_fast, allow_none, filter).boxed_local()
}
coverage,
} => test_command(flags, include, fail_fast, allow_none, filter, coverage)
.boxed_local(),
DenoSubcommand::Completions { buf } => {
if let Err(e) = write_to_stdout_ignore_sigpipe(&buf) {
eprintln!("{}", e);
Expand Down
16 changes: 16 additions & 0 deletions cli/tests/inspector_coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function a() {
console.log("hello a");
}

function b() {
console.log("hello b");
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function c() {
console.log("hello c");
}

a();
b();
b();