diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 92fc58208f38c6..87b8df5f275667 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -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" @@ -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" diff --git a/cli/coverage.rs b/cli/coverage.rs new file mode 100644 index 00000000000000..1c03bebc4d5206 --- /dev/null +++ b/cli/coverage.rs @@ -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, +} + +impl CoverageCollector { + pub async fn connect(url: Url) -> Result { + 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, 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, + 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, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Res { + result: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CoverageResultMsg { + id: usize, + result: Res, +} diff --git a/cli/flags.rs b/cli/flags.rs index 772c719a8849b7..75ac50e2760183 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -68,6 +68,7 @@ pub enum DenoSubcommand { allow_none: bool, include: Option>, filter: Option, + coverage: bool, }, Types, Upgrade { @@ -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 = matches @@ -558,6 +560,7 @@ fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) { include, filter, allow_none, + coverage, }; } @@ -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") @@ -2303,6 +2311,7 @@ mod tests { filter: None, allow_none: true, include: Some(svec!["dir1/", "dir2/"]), + coverage: false, }, allow_read: true, allow_net: true, @@ -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() diff --git a/cli/lib.rs b/cli/lib.rs index 467c05708ff325..9f2e654b66e080 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -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; @@ -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; @@ -482,6 +485,7 @@ async fn test_command( fail_fast: bool, allow_none: bool, filter: Option, + coverage: bool, ) -> Result<(), ErrBox> { let global_state = GlobalState::new(flags.clone())?; let cwd = std::env::current_dir().expect("No current directory"); @@ -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(), @@ -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::>(); + let filtered_report = coverage_report + .into_iter() + .filter(|e| test_modules.contains(&e.url)) + .collect::>(); + eprintln!("test modules {:#?}", test_modules); + eprintln!("coverage report: {:#?}", filtered_report); + } + + Ok(()) } pub fn main() { @@ -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); diff --git a/cli/tests/inspector_coverage.js b/cli/tests/inspector_coverage.js new file mode 100644 index 00000000000000..3db0e5a19b6788 --- /dev/null +++ b/cli/tests/inspector_coverage.js @@ -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();