diff options
27 files changed, 885 insertions, 433 deletions
diff --git a/cli/flags.rs b/cli/flags.rs index 0528a4243..fe88526f4 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -14,7 +14,6 @@ use log::Level; use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; -use tempfile::TempDir; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum DenoSubcommand { @@ -35,6 +34,13 @@ pub enum DenoSubcommand { Completions { buf: Box<[u8]>, }, + Coverage { + files: Vec<PathBuf>, + ignore: Vec<PathBuf>, + include: Vec<String>, + exclude: Vec<String>, + lcov: bool, + }, Doc { private: bool, json: bool, @@ -300,6 +306,8 @@ pub fn flags_from_vec(args: Vec<String>) -> clap::Result<Flags> { types_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("cache") { cache_parse(&mut flags, m); + } else if let Some(m) = matches.subcommand_matches("coverage") { + coverage_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("info") { info_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("eval") { @@ -375,6 +383,7 @@ If the flag is set, restrict these messages to errors.", .subcommand(cache_subcommand()) .subcommand(compile_subcommand()) .subcommand(completions_subcommand()) + .subcommand(coverage_subcommand()) .subcommand(doc_subcommand()) .subcommand(eval_subcommand()) .subcommand(fmt_subcommand()) @@ -569,6 +578,33 @@ fn cache_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.subcommand = DenoSubcommand::Cache { files }; } +fn coverage_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + let files = match matches.values_of("files") { + Some(f) => f.map(PathBuf::from).collect(), + None => vec![], + }; + let ignore = match matches.values_of("ignore") { + Some(f) => f.map(PathBuf::from).collect(), + None => vec![], + }; + let include = match matches.values_of("include") { + Some(f) => f.map(String::from).collect(), + None => vec![], + }; + let exclude = match matches.values_of("exclude") { + Some(f) => f.map(String::from).collect(), + None => vec![], + }; + let lcov = matches.is_present("lcov"); + flags.subcommand = DenoSubcommand::Coverage { + files, + ignore, + include, + exclude, + lcov, + }; +} + fn lock_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { if matches.is_present("lock") { let lockfile = matches.value_of("lock").unwrap(); @@ -672,23 +708,6 @@ fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) { let quiet = matches.is_present("quiet"); let filter = matches.value_of("filter").map(String::from); - flags.coverage_dir = if matches.is_present("coverage") { - if let Some(coverage_dir) = matches.value_of("coverage") { - Some(coverage_dir.to_string()) - } else { - Some( - TempDir::new() - .unwrap() - .into_path() - .to_str() - .unwrap() - .to_string(), - ) - } - } else { - None - }; - if matches.is_present("script_arg") { let script_arg: Vec<String> = matches .values_of("script_arg") @@ -712,6 +731,7 @@ fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) { None }; + flags.coverage_dir = matches.value_of("coverage").map(String::from); flags.subcommand = DenoSubcommand::Test { no_run, fail_fast, @@ -1099,6 +1119,77 @@ Future runs of this module will trigger no downloads or compilation unless ) } +fn coverage_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("coverage") + .about("Print coverage reports") + .long_about( + "Print coverage reports from coverage profiles. + +Collect a coverage profile with deno test: + deno test --coverage=cov_profile + +Print a report to stdout: + deno coverage cov_profile + +Include urls that start with the file schema: + deno coverage --include=\"^file:\" cov_profile + +Exclude urls ending with test.ts and test.js: + deno coverage --exclude=\"test\\.(ts|js)\" cov_profile + +Include urls that start with the file schema and exclude files ending with test.ts and test.js, for +an url to match it must match the include pattern and not match the exclude pattern: + deno coverage --include=\"^file:\" --exclude=\"test\\.(ts|js)\" cov_profile + +Write a report using the lcov format: + deno coverage --lcov cov_profile > cov.lcov + +Generate html reports from lcov: + genhtml -o html_cov cov.lcov +", + ) + .arg( + Arg::with_name("ignore") + .long("ignore") + .takes_value(true) + .use_delimiter(true) + .require_equals(true) + .help("Ignore coverage files"), + ) + .arg( + Arg::with_name("include") + .long("include") + .takes_value(true) + .value_name("regex") + .multiple(true) + .require_equals(true) + .default_value(r"^file:") + .help("Include source files in the report"), + ) + .arg( + Arg::with_name("exclude") + .long("exclude") + .takes_value(true) + .value_name("regex") + .multiple(true) + .require_equals(true) + .default_value(r"test\.(js|mjs|ts|jsx|tsx)$") + .help("Exclude source files from the report"), + ) + .arg( + Arg::with_name("lcov") + .long("lcov") + .help("Output coverage report in lcov format") + .takes_value(false), + ) + .arg( + Arg::with_name("files") + .takes_value(true) + .multiple(true) + .required(true), + ) +} + fn upgrade_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("upgrade") .about("Upgrade deno executable to given version") @@ -1391,14 +1482,12 @@ fn test_subcommand<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("coverage") .long("coverage") - .min_values(0) - .max_values(1) .require_equals(true) .takes_value(true) .requires("unstable") .conflicts_with("inspect") .conflicts_with("inspect-brk") - .help("Collect coverage information"), + .help("Collect coverage profile data"), ) .arg( Arg::with_name("files") @@ -3430,6 +3519,24 @@ mod tests { } #[test] + fn coverage() { + let r = flags_from_vec(svec!["deno", "coverage", "foo.json"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Coverage { + files: vec![PathBuf::from("foo.json")], + ignore: vec![], + include: vec![r"^file:".to_string()], + exclude: vec![r"test\.(js|mjs|ts|jsx|tsx)$".to_string()], + lcov: false, + }, + ..Flags::default() + } + ); + } + + #[test] fn location_with_bad_scheme() { #[rustfmt::skip] let r = flags_from_vec(svec!["deno", "run", "--location", "foo:", "mod.ts"]); diff --git a/cli/main.rs b/cli/main.rs index b9516e704..e957b9342 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -978,6 +978,34 @@ async fn run_command(flags: Flags, script: String) -> Result<(), AnyError> { Ok(()) } +async fn coverage_command( + flags: Flags, + files: Vec<PathBuf>, + ignore: Vec<PathBuf>, + include: Vec<String>, + exclude: Vec<String>, + lcov: bool, +) -> Result<(), AnyError> { + if !flags.unstable { + exit_unstable("compile"); + } + + if files.is_empty() { + println!("No matching coverage profiles found"); + std::process::exit(1); + } + + tools::coverage::cover_files( + flags.clone(), + files, + ignore, + include, + exclude, + lcov, + ) + .await +} + async fn test_command( flags: Flags, include: Option<Vec<String>>, @@ -1047,7 +1075,6 @@ async fn test_command( let mut maybe_coverage_collector = if let Some(ref coverage_dir) = program_state.coverage_dir { let session = worker.create_inspector_session(); - let coverage_dir = PathBuf::from(coverage_dir); let mut coverage_collector = tools::coverage::CoverageCollector::new(coverage_dir, session); @@ -1067,20 +1094,6 @@ async fn test_command( if let Some(coverage_collector) = maybe_coverage_collector.as_mut() { coverage_collector.stop_collecting().await?; - - // TODO(caspervonb) extract reporting into it's own subcommand. - // For now, we'll only report for the command that passed --coverage as a flag. - if flags.coverage_dir.is_some() { - let mut exclude = test_modules.clone(); - exclude.push(main_module.clone()); - tools::coverage::report_coverages( - program_state.clone(), - &coverage_collector.dir, - quiet, - exclude, - ) - .await?; - } } Ok(()) @@ -1172,6 +1185,14 @@ fn get_subcommand( target, } => compile_command(flags, source_file, output, args, target, lite) .boxed_local(), + DenoSubcommand::Coverage { + files, + ignore, + include, + exclude, + lcov, + } => coverage_command(flags, files, ignore, include, exclude, lcov) + .boxed_local(), DenoSubcommand::Fmt { check, files, diff --git a/cli/tests/coverage/branch.ts b/cli/tests/coverage/branch.ts new file mode 100644 index 000000000..352167109 --- /dev/null +++ b/cli/tests/coverage/branch.ts @@ -0,0 +1,15 @@ +export function branch(condition: boolean): boolean { + if (condition) { + return true; + } else { + return false; + } +} + +export function unused(condition: boolean): boolean { + if (condition) { + return false; + } else { + return true; + } +} diff --git a/cli/tests/test_branch_coverage.ts b/cli/tests/coverage/branch_test.ts index 7e3adb737..2a44c8071 100644 --- a/cli/tests/test_branch_coverage.ts +++ b/cli/tests/coverage/branch_test.ts @@ -1,4 +1,4 @@ -import { branch } from "./subdir/branch.ts"; +import { branch } from "./branch.ts"; Deno.test("branch", function () { branch(true); diff --git a/cli/tests/coverage/complex.ts b/cli/tests/coverage/complex.ts new file mode 100644 index 000000000..a54c3437b --- /dev/null +++ b/cli/tests/coverage/complex.ts @@ -0,0 +1,68 @@ +// This entire interface should be completely ignored by the coverage tool. +export interface Complex { + // These comments should be ignored. + foo: string; + + // But this is a stub, so this isn't really documentation. + bar: string; + + // Really all these are doing is padding the line count. + baz: string; +} + +// Lets add some wide characters to ensure that the absolute byte offsets are +// being matched properly. +// +// 패딩에 대한 더 많은 문자. +function dependency( + foo: string, + bar: string, + baz: string, +): Complex { + return { + foo, + bar, + baz, + }; +} + +// Again just more wide characters for padding. +// +// 良い対策のためにいくつかのユニコード文字を投げる。 +export function complex( + foo: string, + bar: string, + baz: string, +): Complex { + return dependency( + foo, + bar, + baz, + ); +} + +// And yet again for good measure. +// 更多用於填充的字元。 +export function unused( + foo: string, + bar: string, + baz: string, +): Complex { + return complex( + foo, + bar, + baz, + ); +} + +// Using a non-ascii name again to ensure that the byte offsets match up +// correctly. +export const π = Math.PI; + +// And same applies for this one, this one is unused and will show up in +// lacking coverage. +export function ƒ(): number { + return ( + 0 + ); +} diff --git a/cli/tests/test_complex_coverage.ts b/cli/tests/coverage/complex_test.ts index 06f17d87d..fda948bc3 100644 --- a/cli/tests/test_complex_coverage.ts +++ b/cli/tests/coverage/complex_test.ts @@ -1,4 +1,4 @@ -import { complex } from "./subdir/complex.ts"; +import { complex } from "./complex.ts"; Deno.test("complex", function () { complex("foo", "bar", "baz"); diff --git a/cli/tests/coverage/expected_branch.lcov b/cli/tests/coverage/expected_branch.lcov new file mode 100644 index 000000000..07e29cca5 --- /dev/null +++ b/cli/tests/coverage/expected_branch.lcov @@ -0,0 +1,27 @@ +SF:[WILDCARD]branch.ts +FN:2,branch +FN:10,unused +FNDA:1,branch +FNDA:0,unused +FNF:2 +FNH:1 +BRDA:4,1,0,0 +BRF:1 +BRH:0 +DA:1,1 +DA:2,2 +DA:3,2 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,1 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +LH:4 +LF:14 +end_of_record diff --git a/cli/tests/coverage/expected_branch.out b/cli/tests/coverage/expected_branch.out new file mode 100644 index 000000000..630ea93b2 --- /dev/null +++ b/cli/tests/coverage/expected_branch.out @@ -0,0 +1,12 @@ +cover [WILDCARD]/coverage/branch.ts ... 28.571% (4/14) + 4 | } else { + 5 | return false; + 6 | } +-----|----- + 9 | export function unused(condition: boolean): boolean { + 10 | if (condition) { + 11 | return false; + 12 | } else { + 13 | return true; + 14 | } + 15 | } diff --git a/cli/tests/coverage/expected_complex.lcov b/cli/tests/coverage/expected_complex.lcov new file mode 100644 index 000000000..0182b3de7 --- /dev/null +++ b/cli/tests/coverage/expected_complex.lcov @@ -0,0 +1,52 @@ +SF:[WILDCARD]complex.ts +FN:22,dependency +FN:37,complex +FN:51,unused +FN:65,ƒ +FNDA:1,dependency +FNDA:1,complex +FNDA:0,unused +FNDA:0,ƒ +FNF:4 +FNH:2 +BRF:0 +BRH:0 +DA:17,2 +DA:18,2 +DA:19,2 +DA:20,2 +DA:22,2 +DA:23,2 +DA:24,2 +DA:25,2 +DA:26,2 +DA:27,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:37,2 +DA:38,2 +DA:39,2 +DA:40,2 +DA:41,2 +DA:42,1 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:60,1 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,1 +LH:22 +LF:36 +end_of_record diff --git a/cli/tests/coverage/expected_complex.out b/cli/tests/coverage/expected_complex.out new file mode 100644 index 000000000..1dee78a87 --- /dev/null +++ b/cli/tests/coverage/expected_complex.out @@ -0,0 +1,17 @@ +cover [WILDCARD]/coverage/complex.ts ... 61.111% (22/36) + 46 | export function unused( + 47 | foo: string, + 48 | bar: string, + 49 | baz: string, +-----|----- + 51 | return complex( + 52 | foo, + 53 | bar, + 54 | baz, + 55 | ); + 56 | } +-----|----- + 64 | export function ƒ(): number { + 65 | return ( + 66 | 0 + 67 | ); diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index e57bca27f..f9f458016 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -3612,48 +3612,6 @@ console.log("finish"); output: "redirect_cache.out", }); - itest!(deno_test_coverage { - args: "test --coverage --unstable test_coverage.ts", - output: "test_coverage.out", - exit_code: 0, - }); - - itest!(deno_test_comment_coverage { - args: "test --coverage --unstable test_comment_coverage.ts", - output: "test_comment_coverage.out", - exit_code: 0, - }); - - itest!(deno_test_branch_coverage { - args: "test --coverage --unstable test_branch_coverage.ts", - output: "test_branch_coverage.out", - exit_code: 0, - }); - - itest!(deno_test_coverage_explicit { - args: "test --coverage=.test_coverage --unstable test_coverage.ts", - output: "test_coverage.out", - exit_code: 0, - }); - - itest!(deno_test_run_test_coverage { - args: "test --allow-all --coverage --unstable test_run_test_coverage.ts", - output: "test_run_test_coverage.out", - exit_code: 0, - }); - - itest!(deno_test_run_run_coverage { - args: "test --allow-all --coverage --unstable test_run_run_coverage.ts", - output: "test_run_run_coverage.out", - exit_code: 0, - }); - - itest!(deno_test_run_combined_coverage { - args: "test --allow-all --coverage --unstable test_run_run_coverage.ts test_run_test_coverage.ts", - output: "test_run_combined_coverage.out", - exit_code: 0, - }); - itest!(deno_lint { args: "lint --unstable lint/file1.js lint/file2.ts lint/ignored_file.ts", output: "lint/expected.out", @@ -3988,6 +3946,160 @@ console.log("finish"); assert_eq!(output.stderr, b""); } + mod coverage { + use super::*; + + #[test] + fn branch() { + let tempdir = TempDir::new().expect("tempdir fail"); + let status = util::deno_cmd() + .current_dir(util::root_path()) + .arg("test") + .arg("--quiet") + .arg("--unstable") + .arg(format!("--coverage={}", tempdir.path().to_str().unwrap())) + .arg("cli/tests/coverage/branch_test.ts") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .status() + .expect("failed to spawn test runner"); + + assert!(status.success()); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("coverage") + .arg("--quiet") + .arg("--unstable") + .arg(format!("{}/", tempdir.path().to_str().unwrap())) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .output() + .expect("failed to spawn coverage reporter"); + + let actual = + util::strip_ansi_codes(std::str::from_utf8(&output.stdout).unwrap()) + .to_string(); + + let expected = fs::read_to_string( + util::root_path().join("cli/tests/coverage/expected_branch.out"), + ) + .unwrap(); + + if !util::wildcard_match(&expected, &actual) { + println!("OUTPUT\n{}\nOUTPUT", actual); + println!("EXPECTED\n{}\nEXPECTED", expected); + panic!("pattern match failed"); + } + + assert!(output.status.success()); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("coverage") + .arg("--quiet") + .arg("--unstable") + .arg("--lcov") + .arg(format!("{}/", tempdir.path().to_str().unwrap())) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .output() + .expect("failed to spawn coverage reporter"); + + let actual = + util::strip_ansi_codes(std::str::from_utf8(&output.stdout).unwrap()) + .to_string(); + + let expected = fs::read_to_string( + util::root_path().join("cli/tests/coverage/expected_branch.lcov"), + ) + .unwrap(); + + if !util::wildcard_match(&expected, &actual) { + println!("OUTPUT\n{}\nOUTPUT", actual); + println!("EXPECTED\n{}\nEXPECTED", expected); + panic!("pattern match failed"); + } + + assert!(output.status.success()); + } + + #[test] + fn complex() { + let tempdir = TempDir::new().expect("tempdir fail"); + let status = util::deno_cmd() + .current_dir(util::root_path()) + .arg("test") + .arg("--quiet") + .arg("--unstable") + .arg(format!("--coverage={}", tempdir.path().to_str().unwrap())) + .arg("cli/tests/coverage/complex_test.ts") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .status() + .expect("failed to spawn test runner"); + + assert!(status.success()); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("coverage") + .arg("--quiet") + .arg("--unstable") + .arg(format!("{}/", tempdir.path().to_str().unwrap())) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .output() + .expect("failed to spawn coverage reporter"); + + let actual = + util::strip_ansi_codes(std::str::from_utf8(&output.stdout).unwrap()) + .to_string(); + + let expected = fs::read_to_string( + util::root_path().join("cli/tests/coverage/expected_complex.out"), + ) + .unwrap(); + + if !util::wildcard_match(&expected, &actual) { + println!("OUTPUT\n{}\nOUTPUT", actual); + println!("EXPECTED\n{}\nEXPECTED", expected); + panic!("pattern match failed"); + } + + assert!(output.status.success()); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("coverage") + .arg("--quiet") + .arg("--unstable") + .arg("--lcov") + .arg(format!("{}/", tempdir.path().to_str().unwrap())) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .output() + .expect("failed to spawn coverage reporter"); + + let actual = + util::strip_ansi_codes(std::str::from_utf8(&output.stdout).unwrap()) + .to_string(); + + let expected = fs::read_to_string( + util::root_path().join("cli/tests/coverage/expected_complex.lcov"), + ) + .unwrap(); + + if !util::wildcard_match(&expected, &actual) { + println!("OUTPUT\n{}\nOUTPUT", actual); + println!("EXPECTED\n{}\nEXPECTED", expected); + panic!("pattern match failed"); + } + + assert!(output.status.success()); + } + } + mod permissions { use super::*; diff --git a/cli/tests/run_coverage.ts b/cli/tests/run_coverage.ts deleted file mode 100644 index d1443dfad..000000000 --- a/cli/tests/run_coverage.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { returnsHi } from "./subdir/mod1.ts"; - -returnsHi(); diff --git a/cli/tests/subdir/branch.ts b/cli/tests/subdir/branch.ts deleted file mode 100644 index bb7aec9eb..000000000 --- a/cli/tests/subdir/branch.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function branch(condition: boolean): boolean { - if (condition) { - return true; - } else { - return false; - } -} diff --git a/cli/tests/subdir/complex.ts b/cli/tests/subdir/complex.ts deleted file mode 100644 index 588e6ce59..000000000 --- a/cli/tests/subdir/complex.ts +++ /dev/null @@ -1,35 +0,0 @@ -// This entire interface should be completely ignored by the coverage tool. -export interface Complex { - // These are comments. - foo: string; - - // But this is a stub, so this isn't really documentation. - bar: string; - - // Really all these are doing is padding the line count. - baz: string; -} - -export function complex( - foo: string, - bar: string, - baz: string, -): Complex { - return { - foo, - bar, - baz, - }; -} - -export function unused( - foo: string, - bar: string, - baz: string, -): Complex { - return complex( - foo, - bar, - baz, - ); -} diff --git a/cli/tests/test_branch_coverage.out b/cli/tests/test_branch_coverage.out deleted file mode 100644 index 375073e36..000000000 --- a/cli/tests/test_branch_coverage.out +++ /dev/null @@ -1,10 +0,0 @@ -Check [WILDCARD]/tests/$deno$test.ts -running 1 tests -test branch ... ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -cover [WILDCARD]/tests/subdir/branch.ts ... 57.143% (4/7) - 4 | } else { - 5 | return false; - 6 | } diff --git a/cli/tests/test_comment_coverage.out b/cli/tests/test_comment_coverage.out deleted file mode 100644 index ce846836c..000000000 --- a/cli/tests/test_comment_coverage.out +++ /dev/null @@ -1,7 +0,0 @@ -[WILDCARD]/tests/$deno$test.ts -running 1 tests -test comment ... ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -[WILDCARD]/tests/subdir/comment.ts ... 100.000% (3/3) diff --git a/cli/tests/test_comment_coverage.ts b/cli/tests/test_comment_coverage.ts deleted file mode 100644 index 28a25c65d..000000000 --- a/cli/tests/test_comment_coverage.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { comment } from "./subdir/comment.ts"; - -Deno.test("comment", function () { - comment(); -}); diff --git a/cli/tests/test_complex_coverage.out b/cli/tests/test_complex_coverage.out deleted file mode 100644 index 1082d098c..000000000 --- a/cli/tests/test_complex_coverage.out +++ /dev/null @@ -1,18 +0,0 @@ -Check [WILDCARD]/tests/$deno$test.ts -running 1 tests -test complex ... ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -cover [WILDCARD]/tests/subdir/complex.ts ... 50.000% (10/20) - 25 | export function unused( - 26 | foo: string, - 27 | bar: string, - 28 | baz: string, ------|----- - 30 | return complex( - 31 | foo, - 32 | bar, - 33 | baz, - 34 | ); - 35 | } diff --git a/cli/tests/test_coverage.out b/cli/tests/test_coverage.out deleted file mode 100644 index 83456bced..000000000 --- a/cli/tests/test_coverage.out +++ /dev/null @@ -1,26 +0,0 @@ -Check [WILDCARD]/$deno$test.ts -running 1 tests -test returnsFooSuccess ... ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -cover [WILDCARD]/tests/subdir/mod1.ts ... 30.769% (4/13) - 3 | export function returnsHi(): string { - 4 | return "Hi"; - 5 | } ------|----- - 11 | export function printHello3(): void { - 12 | printHello2(); - 13 | } ------|----- - 15 | export function throwsError(): void { - 16 | throw Error("exception from mod1"); - 17 | } -cover [WILDCARD]/tests/subdir/print_hello.ts ... 0.000% (0/3) - 1 | export function printHello(): void { - 2 | console.log("Hello"); - 3 | } -cover [WILDCARD]/tests/subdir/subdir2/mod2.ts ... 57.143% (4/7) - 7 | export function printHello2(): void { - 8 | printHello(); - 9 | } diff --git a/cli/tests/test_coverage.ts b/cli/tests/test_coverage.ts deleted file mode 100644 index 0c576d612..000000000 --- a/cli/tests/test_coverage.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { returnsFoo2 } from "./subdir/mod1.ts"; - -Deno.test("returnsFooSuccess", function () { - returnsFoo2(); -}); diff --git a/cli/tests/test_run_combined_coverage.out b/cli/tests/test_run_combined_coverage.out deleted file mode 100644 index 9a638214e..000000000 --- a/cli/tests/test_run_combined_coverage.out +++ /dev/null @@ -1,32 +0,0 @@ -Check [WILDCARD]/tests/$deno$test.ts -running 2 tests -test spawn test ... Check [WILDCARD]/tests/run_coverage.ts -ok ([WILDCARD]) -test spawn test ... Check [WILDCARD]/tests/$deno$test.ts -running 1 tests -test returnsFooSuccess ... ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -ok ([WILDCARD]) - -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -cover [WILDCARD]/tests/run_coverage.ts ... 100.000% (2/2) -cover [WILDCARD]/tests/subdir/mod1.ts ... 53.846% (7/13) - 11 | export function printHello3(): void { - 12 | printHello2(); - 13 | } ------|----- - 15 | export function throwsError(): void { - 16 | throw Error("exception from mod1"); - 17 | } -cover [WILDCARD]/tests/subdir/print_hello.ts ... 0.000% (0/3) - 1 | export function printHello(): void { - 2 | console.log("Hello"); - 3 | } -cover [WILDCARD]/tests/subdir/subdir2/mod2.ts ... 57.143% (4/7) - 7 | export function printHello2(): void { - 8 | printHello(); - 9 | } -cover [WILDCARD]/tests/test_coverage.ts ... 100.000% (4/4) diff --git a/cli/tests/test_run_run_coverage.out b/cli/tests/test_run_run_coverage.out deleted file mode 100644 index 81f86c9be..000000000 --- a/cli/tests/test_run_run_coverage.out +++ /dev/null @@ -1,32 +0,0 @@ -Check [WILDCARD]/tests/$deno$test.ts -running 1 tests -test spawn test ... Check [WILDCARD]/tests/run_coverage.ts -ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -cover [WILDCARD]/tests/run_coverage.ts ... 100.000% (2/2) -cover [WILDCARD]/tests/subdir/mod1.ts ... 30.769% (4/13) - 7 | export function returnsFoo2(): string { - 8 | return returnsFoo(); - 9 | } ------|----- - 11 | export function printHello3(): void { - 12 | printHello2(); - 13 | } ------|----- - 15 | export function throwsError(): void { - 16 | throw Error("exception from mod1"); - 17 | } -cover [WILDCARD]/tests/subdir/print_hello.ts ... 0.000% (0/3) - 1 | export function printHello(): void { - 2 | console.log("Hello"); - 3 | } -cover [WILDCARD]/tests/subdir/subdir2/mod2.ts ... 14.286% (1/7) - 3 | export function returnsFoo(): string { - 4 | return "Foo"; - 5 | } ------|----- - 7 | export function printHello2(): void { - 8 | printHello(); - 9 | } diff --git a/cli/tests/test_run_run_coverage.ts b/cli/tests/test_run_run_coverage.ts deleted file mode 100644 index 448b15e7b..000000000 --- a/cli/tests/test_run_run_coverage.ts +++ /dev/null @@ -1,14 +0,0 @@ -Deno.test("spawn test", async function () { - const process = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--allow-all", - "--unstable", - "run_coverage.ts", - ], - }); - - await process.status(); - process.close(); -}); diff --git a/cli/tests/test_run_test_coverage.out b/cli/tests/test_run_test_coverage.out deleted file mode 100644 index aa524966e..000000000 --- a/cli/tests/test_run_test_coverage.out +++ /dev/null @@ -1,33 +0,0 @@ -Check [WILDCARD]/tests/$deno$test.ts -running 1 tests -test spawn test ... Check [WILDCARD]/tests/$deno$test.ts -running 1 tests -test returnsFooSuccess ... ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -ok ([WILDCARD]) - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]) - -cover [WILDCARD]/tests/subdir/mod1.ts ... 30.769% (4/13) - 3 | export function returnsHi(): string { - 4 | return "Hi"; - 5 | } ------|----- - 11 | export function printHello3(): void { - 12 | printHello2(); - 13 | } ------|----- - 15 | export function throwsError(): void { - 16 | throw Error("exception from mod1"); - 17 | } -cover [WILDCARD]/tests/subdir/print_hello.ts ... 0.000% (0/3) - 1 | export function printHello(): void { - 2 | console.log("Hello"); - 3 | } -cover [WILDCARD]/tests/subdir/subdir2/mod2.ts ... 57.143% (4/7) - 7 | export function printHello2(): void { - 8 | printHello(); - 9 | } -cover [WILDCARD]/tests/test_coverage.ts ... 100.000% (4/4) diff --git a/cli/tests/test_run_test_coverage.ts b/cli/tests/test_run_test_coverage.ts deleted file mode 100644 index e3f0e47ce..000000000 --- a/cli/tests/test_run_test_coverage.ts +++ /dev/null @@ -1,14 +0,0 @@ -Deno.test("spawn test", async function () { - const process = Deno.run({ - cmd: [ - Deno.execPath(), - "test", - "--allow-all", - "--unstable", - "test_coverage.ts", - ], - }); - - await process.status(); - process.close(); -}); diff --git a/cli/tools/coverage.rs b/cli/tools/coverage.rs index f4edd18d4..1ec33affd 100644 --- a/cli/tools/coverage.rs +++ b/cli/tools/coverage.rs @@ -3,6 +3,8 @@ use crate::ast; use crate::ast::TokenOrComment; use crate::colors; +use crate::flags::Flags; +use crate::fs_util::collect_files; use crate::media_type::MediaType; use crate::module_graph::TypeLib; use crate::program_state::ProgramState; @@ -13,12 +15,12 @@ use deno_core::serde_json::json; use deno_core::url::Url; use deno_runtime::inspector::InspectorSession; use deno_runtime::permissions::Permissions; +use regex::Regex; use serde::Deserialize; use serde::Serialize; use sourcemap::SourceMap; use std::fs; use std::path::PathBuf; -use std::sync::Arc; use swc_common::Span; use uuid::Uuid; @@ -34,7 +36,6 @@ impl CoverageCollector { pub async fn start_collecting(&mut self) -> Result<(), AnyError> { self.session.post_message("Debugger.enable", None).await?; - self.session.post_message("Profiler.enable", None).await?; self @@ -66,11 +67,6 @@ impl CoverageCollector { fs::write(self.dir.join(filename), &json)?; } - self - .session - .post_message("Profiler.stopPreciseCoverage", None) - .await?; - self.session.post_message("Profiler.disable", None).await?; self.session.post_message("Debugger.disable", None).await?; @@ -104,7 +100,7 @@ pub struct ScriptCoverage { pub functions: Vec<FunctionCoverage>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct TakePreciseCoverageResult { result: Vec<ScriptCoverage>, @@ -117,17 +113,264 @@ pub struct GetScriptSourceResult { pub bytecode: Option<String>, } -pub struct PrettyCoverageReporter { - quiet: bool, +pub enum CoverageReporterKind { + Pretty, + Lcov, +} + +fn create_reporter( + kind: CoverageReporterKind, +) -> Box<dyn CoverageReporter + Send> { + match kind { + CoverageReporterKind::Lcov => Box::new(LcovCoverageReporter::new()), + CoverageReporterKind::Pretty => Box::new(PrettyCoverageReporter::new()), + } +} + +pub trait CoverageReporter { + fn visit_coverage( + &mut self, + script_coverage: &ScriptCoverage, + script_source: &str, + maybe_source_map: Option<Vec<u8>>, + maybe_original_source: Option<String>, + ); + + fn done(&mut self); +} + +pub struct LcovCoverageReporter {} + +impl LcovCoverageReporter { + pub fn new() -> LcovCoverageReporter { + LcovCoverageReporter {} + } +} + +impl CoverageReporter for LcovCoverageReporter { + fn visit_coverage( + &mut self, + script_coverage: &ScriptCoverage, + script_source: &str, + maybe_source_map: Option<Vec<u8>>, + _maybe_original_source: Option<String>, + ) { + // TODO(caspervonb) cleanup and reduce duplication between reporters, pre-compute line coverage + // elsewhere. + let maybe_source_map = if let Some(source_map) = maybe_source_map { + Some(SourceMap::from_slice(&source_map).unwrap()) + } else { + None + }; + + let url = Url::parse(&script_coverage.url).unwrap(); + let file_path = url.to_file_path().unwrap(); + println!("SF:{}", file_path.to_str().unwrap()); + + let mut functions_found = 0; + for function in &script_coverage.functions { + if function.function_name.is_empty() { + continue; + } + + let source_line = script_source[0..function.ranges[0].start_offset] + .split('\n') + .count(); + + let line_index = if let Some(source_map) = maybe_source_map.as_ref() { + source_map + .tokens() + .find(|token| token.get_dst_line() as usize == source_line) + .map(|token| token.get_src_line() as usize) + .unwrap_or(0) + } else { + source_line + }; + + let function_name = &function.function_name; + + println!("FN:{},{}", line_index + 1, function_name); + + functions_found += 1; + } + + let mut functions_hit = 0; + for function in &script_coverage.functions { + if function.function_name.is_empty() { + continue; + } + + let execution_count = function.ranges[0].count; + let function_name = &function.function_name; + + println!("FNDA:{},{}", execution_count, function_name); + + if execution_count != 0 { + functions_hit += 1; + } + } + + println!("FNF:{}", functions_found); + println!("FNH:{}", functions_hit); + + let mut branches_found = 0; + let mut branches_hit = 0; + for (block_number, function) in script_coverage.functions.iter().enumerate() + { + let block_hits = function.ranges[0].count; + for (branch_number, range) in function.ranges[1..].iter().enumerate() { + let source_line = + script_source[0..range.start_offset].split('\n').count(); + + let line_index = if let Some(source_map) = maybe_source_map.as_ref() { + source_map + .tokens() + .find(|token| token.get_dst_line() as usize == source_line) + .map(|token| token.get_src_line() as usize) + .unwrap_or(0) + } else { + source_line + }; + + // From https://manpages.debian.org/unstable/lcov/geninfo.1.en.html: + // + // Block number and branch number are gcc internal IDs for the branch. Taken is either '-' + // if the basic block containing the branch was never executed or a number indicating how + // often that branch was taken. + // + // However with the data we get from v8 coverage profiles it seems we can't actually hit + // this as appears it won't consider any nested branches it hasn't seen but its here for + // the sake of accuracy. + let taken = if block_hits > 0 { + range.count.to_string() + } else { + "-".to_string() + }; + + println!( + "BRDA:{},{},{},{}", + line_index + 1, + block_number, + branch_number, + taken + ); + + branches_found += 1; + if range.count > 0 { + branches_hit += 1; + } + } + } + + println!("BRF:{}", branches_found); + println!("BRH:{}", branches_hit); + + let lines = script_source.split('\n').collect::<Vec<_>>(); + let line_offsets = { + let mut offsets: Vec<(usize, usize)> = Vec::new(); + let mut index = 0; + + for line in &lines { + offsets.push((index, index + line.len() + 1)); + index += line.len() + 1; + } + + offsets + }; + + let line_counts = line_offsets + .iter() + .map(|(line_start_offset, line_end_offset)| { + let mut count = 0; + + // Count the hits of ranges that include the entire line which will always be at-least one + // as long as the code has been evaluated. + 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; + } + } + } + + // Reset the count if any block intersects with the current line has a count of + // zero. + // + // We check for intersection instead of inclusion here because a block may be anywhere + // inside a line. + for function in &script_coverage.functions { + for range in &function.ranges { + if range.count > 0 { + continue; + } + + if (range.start_offset < *line_start_offset + && range.end_offset > *line_start_offset) + || (range.start_offset < *line_end_offset + && range.end_offset > *line_end_offset) + { + count = 0; + } + } + } + + count + }) + .collect::<Vec<usize>>(); + + let found_lines = if let Some(source_map) = maybe_source_map.as_ref() { + let mut found_lines = line_counts + .iter() + .enumerate() + .map(|(index, count)| { + source_map + .tokens() + .filter(move |token| token.get_dst_line() as usize == index) + .map(move |token| (token.get_src_line() as usize, *count)) + }) + .flatten() + .collect::<Vec<(usize, usize)>>(); + + found_lines.sort_unstable_by_key(|(index, _)| *index); + found_lines.dedup_by_key(|(index, _)| *index); + found_lines + } else { + line_counts + .iter() + .enumerate() + .map(|(index, count)| (index, *count)) + .collect::<Vec<(usize, usize)>>() + }; + + for (index, count) in &found_lines { + println!("DA:{},{}", index + 1, count); + } + + let lines_hit = found_lines.iter().filter(|(_, count)| *count != 0).count(); + + println!("LH:{}", lines_hit); + + let lines_found = found_lines.len(); + println!("LF:{}", lines_found); + + println!("end_of_record"); + } + + fn done(&mut self) {} } -// TODO(caspervonb) add support for lcov output (see geninfo(1) for format spec). +pub struct PrettyCoverageReporter {} + impl PrettyCoverageReporter { - pub fn new(quiet: bool) -> PrettyCoverageReporter { - PrettyCoverageReporter { quiet } + pub fn new() -> PrettyCoverageReporter { + PrettyCoverageReporter {} } +} - pub fn visit_coverage( +impl CoverageReporter for PrettyCoverageReporter { + fn visit_coverage( &mut self, script_coverage: &ScriptCoverage, script_source: &str, @@ -163,6 +406,8 @@ impl PrettyCoverageReporter { offsets }; + // TODO(caspervonb): collect uncovered ranges on the lines so that we can highlight specific + // parts of a line in color (word diff style) instead of the entire line. let line_counts = line_offsets .iter() .enumerate() @@ -241,67 +486,72 @@ impl PrettyCoverageReporter { line_counts }; - if !self.quiet { - print!("cover {} ... ", script_coverage.url); + print!("cover {} ... ", script_coverage.url); - let hit_lines = line_counts - .iter() - .filter(|(_, count)| *count != 0) - .map(|(index, _)| *index); + let hit_lines = line_counts + .iter() + .filter(|(_, count)| *count != 0) + .map(|(index, _)| *index); - let missed_lines = line_counts - .iter() - .filter(|(_, count)| *count == 0) - .map(|(index, _)| *index); + let missed_lines = line_counts + .iter() + .filter(|(_, count)| *count == 0) + .map(|(index, _)| *index); - let lines_found = line_counts.len(); - let lines_hit = hit_lines.count(); - let line_ratio = lines_hit as f32 / lines_found as f32; + let lines_found = line_counts.len(); + let lines_hit = hit_lines.count(); + let line_ratio = lines_hit as f32 / lines_found as f32; - let line_coverage = - format!("{:.3}% ({}/{})", line_ratio * 100.0, lines_hit, lines_found,); + let line_coverage = + format!("{:.3}% ({}/{})", line_ratio * 100.0, lines_hit, lines_found,); - if line_ratio >= 0.9 { - println!("{}", colors::green(&line_coverage)); - } else if line_ratio >= 0.75 { - println!("{}", colors::yellow(&line_coverage)); - } else { - println!("{}", colors::red(&line_coverage)); - } + if line_ratio >= 0.9 { + println!("{}", colors::green(&line_coverage)); + } else if line_ratio >= 0.75 { + println!("{}", colors::yellow(&line_coverage)); + } else { + println!("{}", colors::red(&line_coverage)); + } - let mut last_line = None; - for line_index in missed_lines { - const WIDTH: usize = 4; - const SEPERATOR: &str = "|"; + let mut last_line = None; + for line_index in missed_lines { + const WIDTH: usize = 4; + const SEPERATOR: &str = "|"; - // Put a horizontal separator between disjoint runs of lines - if let Some(last_line) = last_line { - if last_line + 1 != line_index { - let dash = colors::gray(&"-".repeat(WIDTH + 1)); - println!("{}{}{}", dash, colors::gray(SEPERATOR), dash); - } + // Put a horizontal separator between disjoint runs of lines + if let Some(last_line) = last_line { + if last_line + 1 != line_index { + let dash = colors::gray(&"-".repeat(WIDTH + 1)); + println!("{}{}{}", dash, colors::gray(SEPERATOR), dash); } + } - println!( - "{:width$} {} {}", - line_index + 1, - colors::gray(SEPERATOR), - colors::red(&lines[line_index]), - width = WIDTH - ); + println!( + "{:width$} {} {}", + line_index + 1, + colors::gray(SEPERATOR), + colors::red(&lines[line_index]), + width = WIDTH + ); - last_line = Some(line_index); - } + last_line = Some(line_index); } } + + fn done(&mut self) {} } -fn collect_coverages(dir: &PathBuf) -> Result<Vec<ScriptCoverage>, AnyError> { +fn collect_coverages( + files: Vec<PathBuf>, + ignore: Vec<PathBuf>, +) -> Result<Vec<ScriptCoverage>, AnyError> { let mut coverages: Vec<ScriptCoverage> = Vec::new(); + let file_paths = collect_files(&files, &ignore, |file_path| { + file_path.extension().map_or(false, |ext| ext == "json") + })?; - let entries = fs::read_dir(dir)?; - for entry in entries { - let json = fs::read_to_string(entry.unwrap().path())?; + for file_path in file_paths { + let json = fs::read_to_string(file_path.as_path())?; let new_coverage: ScriptCoverage = serde_json::from_str(&json)?; let existing_coverage = @@ -344,49 +594,52 @@ fn collect_coverages(dir: &PathBuf) -> Result<Vec<ScriptCoverage>, AnyError> { fn filter_coverages( coverages: Vec<ScriptCoverage>, - exclude: Vec<Url>, + include: Vec<String>, + exclude: Vec<String>, ) -> Vec<ScriptCoverage> { + let include: Vec<Regex> = + include.iter().map(|e| Regex::new(e).unwrap()).collect(); + + let exclude: Vec<Regex> = + exclude.iter().map(|e| Regex::new(e).unwrap()).collect(); + coverages .into_iter() .filter(|e| { - if let Ok(url) = Url::parse(&e.url) { - if url.path().ends_with("__anonymous__") { - return false; - } + let is_internal = e.url.starts_with("deno:") + || e.url.ends_with("__anonymous__") + || e.url.ends_with("$deno$test.ts"); - for module_url in &exclude { - if &url == module_url { - return false; - } - } + let is_included = include.iter().any(|p| p.is_match(&e.url)); + let is_excluded = exclude.iter().any(|p| p.is_match(&e.url)); - if let Ok(path) = url.to_file_path() { - for module_url in &exclude { - if let Ok(module_path) = module_url.to_file_path() { - if path.starts_with(module_path.parent().unwrap()) { - return true; - } - } - } - } - } - - false + (include.is_empty() || is_included) && !is_excluded && !is_internal }) .collect::<Vec<ScriptCoverage>>() } -pub async fn report_coverages( - program_state: Arc<ProgramState>, - dir: &PathBuf, - quiet: bool, - exclude: Vec<Url>, +pub async fn cover_files( + flags: Flags, + files: Vec<PathBuf>, + ignore: Vec<PathBuf>, + include: Vec<String>, + exclude: Vec<String>, + lcov: bool, ) -> Result<(), AnyError> { - let coverages = collect_coverages(dir)?; - let coverages = filter_coverages(coverages, exclude); + let program_state = ProgramState::build(flags).await?; + + let script_coverages = collect_coverages(files, ignore)?; + let script_coverages = filter_coverages(script_coverages, include, exclude); + + let reporter_kind = if lcov { + CoverageReporterKind::Lcov + } else { + CoverageReporterKind::Pretty + }; - let mut coverage_reporter = PrettyCoverageReporter::new(quiet); - for script_coverage in coverages { + let mut reporter = create_reporter(reporter_kind); + + for script_coverage in script_coverages { let module_specifier = deno_core::resolve_url_or_path(&script_coverage.url)?; program_state @@ -408,7 +661,7 @@ pub async fn report_coverages( .get_source(&module_specifier) .map(|f| f.source); - coverage_reporter.visit_coverage( + reporter.visit_coverage( &script_coverage, &script_source, maybe_source_map, @@ -416,5 +669,7 @@ pub async fn report_coverages( ); } + reporter.done(); + Ok(()) } diff --git a/docs/testing.md b/docs/testing.md index 9b53c4c82..9d9f2de04 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -234,29 +234,36 @@ deno test --fail-fast ## Test coverage -Deno will automatically determine test coverage for your code if you specify the -`--coverage` flag when starting `deno test`. Coverage is determined on a line by -line basis for modules that share the parent directory with at-least one test -module that is being executed. +Deno will collect test coverage into a directory for your code if you specify +the `--coverage` flag when starting `deno test`. -This coverage information is acquired directly from the JavaScript engine (V8). -Because of this, the coverage reports are very accurate. +This coverage information is acquired directly from the JavaScript engine (V8) +which is very accurate. -When all tests are done running a summary of coverage per file is printed to -stdout. In the future there will be support for `lcov` output too. +This can then be further processed from the internal format into well known +formats by the `deno coverage` tool. ``` -$ git clone git@github.com:denosaurs/deno_brotli.git && cd deno_brotli -$ deno test --coverage --unstable -Debugger listening on ws://127.0.0.1:9229/ws/5a593019-d185-478b-a928-ebc33e5834be -Check file:///home/deno/deno_brotli/$deno$test.ts -running 2 tests -test compress ... ok (26ms) -test decompress ... ok (13ms) - -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (40ms) - -test coverage: -file:///home/deno/deno_brotli/mod.ts 100.000% -file:///home/deno/deno_brotli/wasm.js 100.000% +# Go into your project's working directory +git clone https://github.com/oakserver/oak && cd oak + +# Collect your coverage profile with deno test --coverage=<output_directory> +deno test --coverage=cov_profile --unstable + +# From this you can get a pretty printed diff of uncovered lines +deno coverage --unstable cov_profile + +# Or generate an lcov report +deno coverage --unstable cov_profile --lcov > cov_profile.lcov + +# Which can then be further processed by tools like genhtml +genhtml -o cov_profile/html cov_profile.lcov ``` + +By default, `deno coverage` will exclude any files matching the regular +expression `test\.(js|mjs|ts|jsx|tsx)` and only consider including files +matching the regular expression `^file:`. + +These filters can be overriden using the `--exclude` and `--include` flags. A +source file's url must match both regular expressions for it to be a part of the +report. |