feat(test): create coverage reports when --coverage specified in deno test (#28260)

This PR updates the behavior of `deno test --coverage` option. Now if
`--coverage` option is specified, `deno test` command automatically
shows summary report in the terminal, and generates the lcov report in
`$coverage_dir/lcov.info` and html report in `$coverage_dir/html/`

This change also adds `--coverage-raw-data-only` flag, which prevents
the above reports generated, instead only generates the raw json
coverage data (which is the same as current behavior)
This commit is contained in:
Yoshiya Hinosawa 2025-04-18 18:56:14 +09:00 committed by GitHub
parent 0bb16651c0
commit 83f15ece09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 203 additions and 36 deletions

View file

@ -27,6 +27,7 @@ use deno_error::JsErrorBox;
use deno_resolver::npm::DenoInNpmPackageChecker;
use node_resolver::InNpmPackageChecker;
use regex::Regex;
use reporter::CoverageReporter;
use text_lines::TextLines;
use uuid::Uuid;
@ -35,7 +36,6 @@ use self::ignore_directives::lex_comments;
use self::ignore_directives::parse_next_ignore_directives;
use self::ignore_directives::parse_range_ignore_directives;
use crate::args::CliOptions;
use crate::args::CoverageFlags;
use crate::args::FileFlags;
use crate::args::Flags;
use crate::cdp;
@ -49,7 +49,7 @@ use crate::util::text_encoding::source_map_from_code;
mod ignore_directives;
mod merge;
mod range_tree;
mod reporter;
pub mod reporter;
mod util;
use merge::ProcessCoverage;
@ -596,9 +596,14 @@ fn filter_coverages(
pub fn cover_files(
flags: Arc<Flags>,
coverage_flags: CoverageFlags,
files_include: Vec<String>,
files_ignore: Vec<String>,
include: Vec<String>,
exclude: Vec<String>,
output: Option<String>,
reporters: &[&dyn CoverageReporter],
) -> Result<(), AnyError> {
if coverage_flags.files.include.is_empty() {
if files_include.is_empty() {
return Err(anyhow!("No matching coverage profiles found"));
}
@ -609,26 +614,21 @@ pub fn cover_files(
let emitter = factory.emitter()?;
let cjs_tracker = factory.cjs_tracker()?;
assert!(!coverage_flags.files.include.is_empty());
// Use the first include path as the default output path.
let coverage_root = cli_options
.initial_cwd()
.join(&coverage_flags.files.include[0]);
let coverage_root = cli_options.initial_cwd().join(&files_include[0]);
let script_coverages = collect_coverages(
cli_options,
coverage_flags.files,
FileFlags {
include: files_include,
ignore: files_ignore,
},
cli_options.initial_cwd(),
)?;
if script_coverages.is_empty() {
return Err(anyhow!("No coverage files found"));
}
let script_coverages = filter_coverages(
script_coverages,
coverage_flags.include,
coverage_flags.exclude,
in_npm_pkg_checker,
);
let script_coverages =
filter_coverages(script_coverages, include, exclude, in_npm_pkg_checker);
if script_coverages.is_empty() {
return Err(anyhow!("No covered files included in the report"));
}
@ -645,9 +645,7 @@ pub fn cover_files(
vec![]
};
let mut reporter = reporter::create(coverage_flags.r#type);
let out_mode = match coverage_flags.output {
let out_mode = match output {
Some(ref path) => match File::create(path) {
Ok(_) => Some(PathBuf::from(path)),
Err(e) => {
@ -742,7 +740,9 @@ pub fn cover_files(
}
}
reporter.done(&coverage_root, &file_reports);
for reporter in reporters {
reporter.done(&coverage_root, &file_reports);
}
Ok(())
}

View file

@ -42,7 +42,7 @@ pub fn create(kind: CoverageType) -> Box<dyn CoverageReporter + Send> {
pub trait CoverageReporter {
fn done(
&mut self,
&self,
coverage_root: &Path,
file_reports: &[(CoverageReport, String)],
);
@ -105,7 +105,7 @@ pub trait CoverageReporter {
}
}
struct SummaryCoverageReporter {}
pub struct SummaryCoverageReporter {}
#[allow(clippy::print_stdout)]
impl SummaryCoverageReporter {
@ -172,7 +172,7 @@ impl SummaryCoverageReporter {
#[allow(clippy::print_stdout)]
impl CoverageReporter for SummaryCoverageReporter {
fn done(
&mut self,
&self,
_coverage_root: &Path,
file_reports: &[(CoverageReport, String)],
) {
@ -206,17 +206,30 @@ impl CoverageReporter for SummaryCoverageReporter {
}
}
struct LcovCoverageReporter {}
pub struct LcovCoverageReporter {}
impl CoverageReporter for LcovCoverageReporter {
fn done(
&mut self,
&self,
_coverage_root: &Path,
file_reports: &[(CoverageReport, String)],
) {
file_reports.iter().for_each(|(report, file_text)| {
self.report(report, file_text).unwrap();
});
if let Some((report, _)) = file_reports.first() {
if let Some(ref output) = report.output {
if let Ok(path) = output.canonicalize() {
let url = Url::from_file_path(path).unwrap();
log::info!("Lcov coverage report has been generated at {}", url);
} else {
log::error!(
"Failed to resolve the output path of Lcov report: {}",
output.display()
);
}
}
}
}
}
@ -226,7 +239,7 @@ impl LcovCoverageReporter {
}
fn report(
&mut self,
&self,
coverage_report: &CoverageReport,
_file_text: &str,
) -> Result<(), AnyError> {
@ -320,7 +333,7 @@ struct DetailedCoverageReporter {}
impl CoverageReporter for DetailedCoverageReporter {
fn done(
&mut self,
&self,
_coverage_root: &Path,
file_reports: &[(CoverageReport, String)],
) {
@ -337,7 +350,7 @@ impl DetailedCoverageReporter {
}
fn report(
&mut self,
&self,
coverage_report: &CoverageReport,
file_text: &str,
) -> Result<(), AnyError> {
@ -398,11 +411,11 @@ impl DetailedCoverageReporter {
}
}
struct HtmlCoverageReporter {}
pub struct HtmlCoverageReporter {}
impl CoverageReporter for HtmlCoverageReporter {
fn done(
&mut self,
&self,
coverage_root: &Path,
file_reports: &[(CoverageReport, String)],
) {