feat(cli): add --coverage flag to deno run command (#29329)

closes #16440

This PR adds `--coverage` flag to `deno run` command. When the flag is
specified, it generates the coverage profile in the directory specified
(default is `coverage`). The coverage directory can also be specified
from the env var `DENO_COVERAGE_DIR`.

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Yoshiya Hinosawa 2025-06-25 20:34:30 +09:00 committed by GitHub
parent bff09506bd
commit 56d7660ee4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 142 additions and 0 deletions

View file

@ -336,6 +336,7 @@ pub struct RunFlags {
pub script: String, pub script: String,
pub watch: Option<WatchFlagsWithPaths>, pub watch: Option<WatchFlagsWithPaths>,
pub bare: bool, pub bare: bool,
pub coverage_dir: Option<String>,
} }
impl RunFlags { impl RunFlags {
@ -345,6 +346,7 @@ impl RunFlags {
script, script,
watch: None, watch: None,
bare: false, bare: false,
coverage_dir: None,
} }
} }
@ -3306,6 +3308,7 @@ fn run_args(command: Command, top_level: bool) -> Command {
}) })
.arg(env_file_arg()) .arg(env_file_arg())
.arg(no_code_cache_arg()) .arg(no_code_cache_arg())
.arg(coverage_arg())
} }
fn run_subcommand() -> Command { fn run_subcommand() -> Command {
@ -4475,6 +4478,21 @@ fn no_code_cache_arg() -> Arg {
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
} }
fn coverage_arg() -> Arg {
Arg::new("coverage")
.long("coverage")
.value_name("DIR")
.num_args(0..=1)
.require_equals(true)
.default_missing_value("coverage")
.conflicts_with("inspect")
.conflicts_with("inspect-wait")
.conflicts_with("inspect-brk")
.help(cstr!("Collect coverage profile data into DIR. If DIR is not specified, it uses 'coverage/'.
<p(245)>This option can also be set via the DENO_COVERAGE_DIR environment variable."))
.value_hint(ValueHint::AnyPath)
}
fn permit_no_files_arg() -> Arg { fn permit_no_files_arg() -> Arg {
Arg::new("permit-no-files") Arg::new("permit-no-files")
.long("permit-no-files") .long("permit-no-files")
@ -5600,6 +5618,7 @@ fn run_parse(
ext_arg_parse(flags, matches); ext_arg_parse(flags, matches);
flags.code_cache_enabled = !matches.get_flag("no-code-cache"); flags.code_cache_enabled = !matches.get_flag("no-code-cache");
let coverage_dir = matches.remove_one::<String>("coverage");
if let Some(mut script_arg) = matches.remove_many::<String>("script_arg") { if let Some(mut script_arg) = matches.remove_many::<String>("script_arg") {
let script = script_arg.next().unwrap(); let script = script_arg.next().unwrap();
@ -5608,6 +5627,7 @@ fn run_parse(
script, script,
watch: watch_arg_parse_with_paths(matches)?, watch: watch_arg_parse_with_paths(matches)?,
bare, bare,
coverage_dir,
}); });
} else if bare { } else if bare {
return Err(app.override_usage("deno [OPTIONS] [COMMAND] [SCRIPT_ARG]...").error( return Err(app.override_usage("deno [OPTIONS] [COMMAND] [SCRIPT_ARG]...").error(
@ -6566,6 +6586,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6591,6 +6612,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: true, bare: true,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6617,6 +6639,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6643,6 +6666,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6669,6 +6693,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6696,6 +6721,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: true, bare: true,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6726,6 +6752,7 @@ mod tests {
exclude: vec![], exclude: vec![],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6755,6 +6782,7 @@ mod tests {
exclude: vec![String::from("foo")], exclude: vec![String::from("foo")],
}), }),
bare: true, bare: true,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6781,6 +6809,7 @@ mod tests {
exclude: vec![String::from("bar")], exclude: vec![String::from("bar")],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6808,6 +6837,7 @@ mod tests {
exclude: vec![String::from("foo"), String::from("bar")], exclude: vec![String::from("foo"), String::from("bar")],
}), }),
bare: false, bare: false,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6834,6 +6864,7 @@ mod tests {
exclude: vec![String::from("baz"), String::from("qux"),], exclude: vec![String::from("baz"), String::from("qux"),],
}), }),
bare: true, bare: true,
coverage_dir: None,
}), }),
code_cache_enabled: true, code_cache_enabled: true,
..Flags::default() ..Flags::default()
@ -6862,6 +6893,24 @@ mod tests {
); );
} }
#[test]
fn run_coverage() {
let r = flags_from_vec(svec!["deno", "run", "--coverage=foo", "script.ts"]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Run(RunFlags {
script: "script.ts".to_string(),
watch: None,
bare: false,
coverage_dir: Some("foo".to_string()),
}),
code_cache_enabled: true,
..Flags::default()
}
);
}
#[test] #[test]
fn run_v8_flags() { fn run_v8_flags() {
let r = flags_from_vec(svec!["deno", "run", "--v8-flags=--help"]); let r = flags_from_vec(svec!["deno", "run", "--v8-flags=--help"]);
@ -7170,6 +7219,7 @@ mod tests {
script: "gist.ts".to_string(), script: "gist.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
permissions: PermissionFlags { permissions: PermissionFlags {
deny_read: Some(vec![]), deny_read: Some(vec![]),
@ -8456,6 +8506,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
permissions: PermissionFlags { permissions: PermissionFlags {
deny_net: Some(svec!["127.0.0.1"]), deny_net: Some(svec!["127.0.0.1"]),
@ -8643,6 +8694,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
permissions: PermissionFlags { permissions: PermissionFlags {
deny_sys: Some(svec!["hostname"]), deny_sys: Some(svec!["hostname"]),
@ -8942,6 +8994,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
..Flags::default() ..Flags::default()
} }
@ -9252,6 +9305,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
log_level: Some(Level::Error), log_level: Some(Level::Error),
code_cache_enabled: true, code_cache_enabled: true,
@ -9372,6 +9426,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
type_check_mode: TypeCheckMode::None, type_check_mode: TypeCheckMode::None,
code_cache_enabled: true, code_cache_enabled: true,
@ -9540,6 +9595,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
node_modules_dir: Some(NodeModulesDirMode::Auto), node_modules_dir: Some(NodeModulesDirMode::Auto),
code_cache_enabled: true, code_cache_enabled: true,
@ -10763,6 +10819,7 @@ mod tests {
script: "foo.js".to_string(), script: "foo.js".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
inspect_wait: Some("127.0.0.1:9229".parse().unwrap()), inspect_wait: Some("127.0.0.1:9229".parse().unwrap()),
code_cache_enabled: true, code_cache_enabled: true,
@ -11453,6 +11510,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
type_check_mode: TypeCheckMode::None, type_check_mode: TypeCheckMode::None,
code_cache_enabled: true, code_cache_enabled: true,
@ -12006,6 +12064,7 @@ mod tests {
script: "script.ts".to_string(), script: "script.ts".to_string(),
watch: None, watch: None,
bare: true, bare: true,
coverage_dir: None,
}), }),
config_flag: ConfigFlag::Disabled, config_flag: ConfigFlag::Disabled,
code_cache_enabled: true, code_cache_enabled: true,

View file

@ -762,6 +762,11 @@ impl CliOptions {
.as_ref() .as_ref()
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.or_else(|| env::var("DENO_COVERAGE_DIR").ok()), .or_else(|| env::var("DENO_COVERAGE_DIR").ok()),
DenoSubcommand::Run(flags) => flags
.coverage_dir
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| env::var("DENO_COVERAGE_DIR").ok()),
_ => None, _ => None,
} }
} }

View file

@ -579,6 +579,7 @@ fn filter_coverages(
|| e.url.starts_with("data:") || e.url.starts_with("data:")
|| e.url.ends_with("__anonymous__") || e.url.ends_with("__anonymous__")
|| e.url.ends_with("$deno$test.mjs") || e.url.ends_with("$deno$test.mjs")
|| e.url.ends_with("$deno$stdin.mts")
|| e.url.ends_with(".snap") || e.url.ends_with(".snap")
|| is_supported_test_path(Path::new(e.url.as_str())) || is_supported_test_path(Path::new(e.url.as_str()))
|| doc_test_re.is_match(e.url.as_str()) || doc_test_re.is_match(e.url.as_str())

View file

@ -37,6 +37,7 @@ pub async fn deploy(
), ),
watch: None, watch: None,
bare: false, bare: false,
coverage_dir: None,
}); });
tools::run::run_script( tools::run::run_script(

View file

@ -0,0 +1,36 @@
{
"tempDir": true,
"tests": {
"run_coverage": {
"steps": [
{
"args": "run --coverage foo.ts",
"output": "0\n0\n"
},
{
"args": "coverage",
"output": "coverage_summary.out"
}
]
},
"run_coverage_env": {
"steps": [
{
"args": "run foo.ts",
"output": "0\n0\n",
"envs": {
"DENO_COVERAGE_DIR": "my_coverage_dir"
}
},
{
"args": "coverage my_coverage_dir",
"output": "coverage_summary.out"
}
]
},
"test_child_process_coverage": {
"args": "test -A --coverage child_process_coverage_test.ts",
"output": "child_process_coverage_test.out"
}
}
}

View file

@ -0,0 +1,13 @@
Check file:///[WILDLINE]child_process_coverage_test.ts
running 2 tests from ./child_process_coverage_test.ts
foo 1 0 ... ok ([WILDLINE]s)
foo 0 1 ... ok ([WILDLINE]s)
ok | 2 passed | 0 failed ([WILDLINE]s)
| File | Branch % | Line % |
| --------- | -------- | ------ |
| foo.ts | 100.0 | 100.0 |
| All files | 100.0 | 100.0 |
Lcov coverage report has been generated at file:///[WILDLINE]/coverage/lcov.info
HTML coverage report has been generated at file:///[WILDLINE]/coverage/html/index.html

View file

@ -0,0 +1,6 @@
Deno.test("foo 1 0", () => runDenoCommand("run foo.ts 1 0"));
Deno.test("foo 0 1", () => runDenoCommand("run foo.ts 0 1"));
async function runDenoCommand(args: string) {
await new Deno.Command(Deno.execPath(), { args: args.split(" ") }).output();
}

View file

@ -0,0 +1,4 @@
| File | Branch % | Line % |
| --------- | -------- | ------ |
| foo.ts | 0.0 | 57.1 |
| All files | 0.0 | 57.1 |

View file

@ -0,0 +1,17 @@
function foo(a: number, b: number) {
if (a > 0) {
console.log(a);
} else {
console.log(a);
}
if (b > 0) {
console.log(b);
} else {
console.log(b);
}
}
const [a = 0, b = 0] = Deno.args;
foo(+a, +b);