mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
feat: add "deno coverage" subcommand (#8664)
This commit adds a new subcommand called "coverage" which can generate code coverage reports to stdout in multiple formats from code coverage profiles collected to disk. Currently this supports outputting a pretty printed diff and the lcov format for interoperability with third-party services and tools. Code coverage is still collected via other subcommands that run and collect code coverage such as "deno test --coverage=<directory>" but that command no longer prints a pretty printed report at the end of a test run with coverage collection enabled. The restrictions on which files that can be reported on has also been relaxed and are fully controllable with the include and exclude regular expression flags on the coverage subcommand. Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
This commit is contained in:
parent
f6a80f34d9
commit
ae8874b4b2
27 changed files with 885 additions and 433 deletions
149
cli/flags.rs
149
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")
|
||||
|
@ -3429,6 +3518,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]
|
||||
|
|
51
cli/main.rs
51
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,
|
||||
|
|
15
cli/tests/coverage/branch.ts
Normal file
15
cli/tests/coverage/branch.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { branch } from "./subdir/branch.ts";
|
||||
import { branch } from "./branch.ts";
|
||||
|
||||
Deno.test("branch", function () {
|
||||
branch(true);
|
68
cli/tests/coverage/complex.ts
Normal file
68
cli/tests/coverage/complex.ts
Normal file
|
@ -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
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { complex } from "./subdir/complex.ts";
|
||||
import { complex } from "./complex.ts";
|
||||
|
||||
Deno.test("complex", function () {
|
||||
complex("foo", "bar", "baz");
|
27
cli/tests/coverage/expected_branch.lcov
Normal file
27
cli/tests/coverage/expected_branch.lcov
Normal file
|
@ -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
|
12
cli/tests/coverage/expected_branch.out
Normal file
12
cli/tests/coverage/expected_branch.out
Normal file
|
@ -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 | }
|
52
cli/tests/coverage/expected_complex.lcov
Normal file
52
cli/tests/coverage/expected_complex.lcov
Normal file
|
@ -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
|
17
cli/tests/coverage/expected_complex.out
Normal file
17
cli/tests/coverage/expected_complex.out
Normal file
|
@ -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 | );
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import { returnsHi } from "./subdir/mod1.ts";
|
||||
|
||||
returnsHi();
|
|
@ -1,7 +0,0 @@
|
|||
export function branch(condition: boolean): boolean {
|
||||
if (condition) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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 | }
|
|
@ -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)
|
|
@ -1,5 +0,0 @@
|
|||
import { comment } from "./subdir/comment.ts";
|
||||
|
||||
Deno.test("comment", function () {
|
||||
comment();
|
||||
});
|
|
@ -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 | }
|
|
@ -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 | }
|
|
@ -1,5 +0,0 @@
|
|||
import { returnsFoo2 } from "./subdir/mod1.ts";
|
||||
|
||||
Deno.test("returnsFooSuccess", function () {
|
||||
returnsFoo2();
|
||||
});
|
|
@ -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)
|
|
@ -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 | }
|
|
@ -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();
|
||||
});
|
|
@ -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)
|
|
@ -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();
|
||||
});
|
|
@ -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,
|
||||
}
|
||||
|
||||
// TODO(caspervonb) add support for lcov output (see geninfo(1) for format spec).
|
||||
impl PrettyCoverageReporter {
|
||||
pub fn new(quiet: bool) -> PrettyCoverageReporter {
|
||||
PrettyCoverageReporter { quiet }
|
||||
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");
|
||||
}
|
||||
|
||||
pub fn visit_coverage(
|
||||
fn done(&mut self) {}
|
||||
}
|
||||
|
||||
pub struct PrettyCoverageReporter {}
|
||||
|
||||
impl PrettyCoverageReporter {
|
||||
pub fn new() -> PrettyCoverageReporter {
|
||||
PrettyCoverageReporter {}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
last_line = Some(line_index);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{:width$} {} {}",
|
||||
line_index + 1,
|
||||
colors::gray(SEPERATOR),
|
||||
colors::red(&lines[line_index]),
|
||||
width = WIDTH
|
||||
);
|
||||
|
||||
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 mut coverage_reporter = PrettyCoverageReporter::new(quiet);
|
||||
for script_coverage in coverages {
|
||||
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 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(())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
# Go into your project's working directory
|
||||
git clone https://github.com/oakserver/oak && cd oak
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (40ms)
|
||||
# Collect your coverage profile with deno test --coverage=<output_directory>
|
||||
deno test --coverage=cov_profile --unstable
|
||||
|
||||
test coverage:
|
||||
file:///home/deno/deno_brotli/mod.ts 100.000%
|
||||
file:///home/deno/deno_brotli/wasm.js 100.000%
|
||||
# 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.
|
||||
|
|
Loading…
Add table
Reference in a new issue