diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/Cargo.toml | 16 | ||||
-rw-r--r-- | cli/build.rs | 574 | ||||
-rw-r--r-- | cli/js.rs | 57 | ||||
-rw-r--r-- | cli/js/40_testing.js | 1412 | ||||
-rw-r--r-- | cli/lsp/testing/execution.rs | 4 | ||||
-rw-r--r-- | cli/main.rs | 35 | ||||
-rw-r--r-- | cli/standalone.rs | 2 | ||||
-rw-r--r-- | cli/tests/testdata/test/steps/failing_steps.out | 8 | ||||
-rw-r--r-- | cli/tools/bench.rs | 4 | ||||
-rw-r--r-- | cli/tools/test.rs | 4 | ||||
-rw-r--r-- | cli/worker.rs | 267 |
11 files changed, 2035 insertions, 348 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 83ff3d77e..732d586d2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,25 +26,16 @@ harness = false path = "./bench/lsp_bench_standalone.rs" [build-dependencies] -deno_broadcast_channel = { version = "0.72.0", path = "../ext/broadcast_channel" } -deno_cache = { version = "0.10.0", path = "../ext/cache" } -deno_console = { version = "0.78.0", path = "../ext/console" } +deno_runtime = { version = "0.86.0", path = "../runtime" } deno_core = { version = "0.160.0", path = "../core" } -deno_crypto = { version = "0.92.0", path = "../ext/crypto" } -deno_fetch = { version = "0.101.0", path = "../ext/fetch" } -deno_net = { version = "0.70.0", path = "../ext/net" } -deno_node = { version = "0.15.0", path = "../ext/node" } -deno_url = { version = "0.78.0", path = "../ext/url" } -deno_web = { version = "0.109.0", path = "../ext/web" } -deno_webgpu = { version = "0.79.0", path = "../ext/webgpu" } -deno_websocket = { version = "0.83.0", path = "../ext/websocket" } -deno_webstorage = { version = "0.73.0", path = "../ext/webstorage" } regex = "=1.6.0" serde = { version = "=1.0.144", features = ["derive"] } serde_json = "1.0.64" zstd = '=0.11.2' glibc_version = "0.1.2" +lzzzz = '1.0' + [target.'cfg(windows)'.build-dependencies] winapi = "=0.3.9" winres = "=0.1.12" @@ -86,6 +77,7 @@ jsonc-parser = { version = "=0.21.0", features = ["serde"] } libc = "=0.2.126" log = { version = "=0.4.17", features = ["serde"] } lsp-types = "=0.93.2" # used by tower-lsp and "proposed" feature is unstable in patch releases +lzzzz = '1.0' mitata = "=0.0.7" monch = "=0.4.0" notify = "=5.0.0" diff --git a/cli/build.rs b/cli/build.rs index 73d0208f6..c7d902941 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -1,306 +1,319 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -use deno_core::error::custom_error; -use deno_core::error::AnyError; -use deno_core::op; -use deno_core::serde::Deserialize; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; use deno_core::Extension; -use deno_core::JsRuntime; -use deno_core::OpState; -use deno_core::RuntimeOptions; -use regex::Regex; -use std::collections::HashMap; use std::env; use std::path::Path; use std::path::PathBuf; -// TODO(bartlomieju): this module contains a lot of duplicated -// logic with `runtime/build.rs`, factor out to `deno_core`. -fn create_snapshot( - mut js_runtime: JsRuntime, - snapshot_path: &Path, - files: Vec<PathBuf>, -) { - // TODO(nayeemrmn): https://github.com/rust-lang/cargo/issues/3946 to get the - // workspace root. - let display_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); - for file in files { - println!("cargo:rerun-if-changed={}", file.display()); - let display_path = file.strip_prefix(display_root).unwrap(); - let display_path_str = display_path.display().to_string(); - js_runtime - .execute_script( - &("deno:".to_string() + &display_path_str.replace('\\', "/")), - &std::fs::read_to_string(&file).unwrap(), - ) - .unwrap(); +use deno_core::snapshot_util::*; +use deno_runtime::deno_cache::SqliteBackedCache; +use deno_runtime::permissions::Permissions; +use deno_runtime::*; + +mod ts { + use super::*; + use crate::deno_webgpu_get_declaration; + use deno_core::error::custom_error; + use deno_core::error::AnyError; + use deno_core::op; + use deno_core::OpState; + use regex::Regex; + use serde::Deserialize; + use serde_json::json; + use serde_json::Value; + use std::collections::HashMap; + use std::path::Path; + use std::path::PathBuf; + + #[derive(Debug, Deserialize)] + struct LoadArgs { + /// The fully qualified specifier that should be loaded. + specifier: String, } - let snapshot = js_runtime.snapshot(); - let snapshot_slice: &[u8] = &snapshot; - println!("Snapshot size: {}", snapshot_slice.len()); - - let compressed_snapshot_with_size = { - let mut vec = vec![]; - - vec.extend_from_slice( - &u32::try_from(snapshot.len()) - .expect("snapshot larger than 4gb") - .to_le_bytes(), + pub fn create_compiler_snapshot( + snapshot_path: PathBuf, + files: Vec<PathBuf>, + cwd: &Path, + ) { + // libs that are being provided by op crates. + let mut op_crate_libs = HashMap::new(); + op_crate_libs.insert("deno.cache", deno_cache::get_declaration()); + op_crate_libs.insert("deno.console", deno_console::get_declaration()); + op_crate_libs.insert("deno.url", deno_url::get_declaration()); + op_crate_libs.insert("deno.web", deno_web::get_declaration()); + op_crate_libs.insert("deno.fetch", deno_fetch::get_declaration()); + op_crate_libs.insert("deno.webgpu", deno_webgpu_get_declaration()); + op_crate_libs.insert("deno.websocket", deno_websocket::get_declaration()); + op_crate_libs.insert("deno.webstorage", deno_webstorage::get_declaration()); + op_crate_libs.insert("deno.crypto", deno_crypto::get_declaration()); + op_crate_libs.insert( + "deno.broadcast_channel", + deno_broadcast_channel::get_declaration(), ); + op_crate_libs.insert("deno.net", deno_net::get_declaration()); - vec.extend_from_slice( - &zstd::bulk::compress(snapshot_slice, 22) - .expect("snapshot compression failed"), - ); - - vec - }; - - println!( - "Snapshot compressed size: {}", - compressed_snapshot_with_size.len() - ); - - std::fs::write(snapshot_path, compressed_snapshot_with_size).unwrap(); - println!("Snapshot written to: {} ", snapshot_path.display()); -} - -#[derive(Debug, Deserialize)] -struct LoadArgs { - /// The fully qualified specifier that should be loaded. - specifier: String, -} - -fn create_compiler_snapshot( - snapshot_path: &Path, - files: Vec<PathBuf>, - cwd: &Path, -) { - // libs that are being provided by op crates. - let mut op_crate_libs = HashMap::new(); - op_crate_libs.insert("deno.cache", deno_cache::get_declaration()); - op_crate_libs.insert("deno.console", deno_console::get_declaration()); - op_crate_libs.insert("deno.url", deno_url::get_declaration()); - op_crate_libs.insert("deno.web", deno_web::get_declaration()); - op_crate_libs.insert("deno.fetch", deno_fetch::get_declaration()); - op_crate_libs.insert("deno.webgpu", deno_webgpu_get_declaration()); - op_crate_libs.insert("deno.websocket", deno_websocket::get_declaration()); - op_crate_libs.insert("deno.webstorage", deno_webstorage::get_declaration()); - op_crate_libs.insert("deno.crypto", deno_crypto::get_declaration()); - op_crate_libs.insert( - "deno.broadcast_channel", - deno_broadcast_channel::get_declaration(), - ); - op_crate_libs.insert("deno.net", deno_net::get_declaration()); - - // ensure we invalidate the build properly. - for (_, path) in op_crate_libs.iter() { - println!("cargo:rerun-if-changed={}", path.display()); - } - - // libs that should be loaded into the isolate before snapshotting. - let libs = vec![ - // Deno custom type libraries - "deno.window", - "deno.worker", - "deno.shared_globals", - "deno.ns", - "deno.unstable", - // Deno built-in type libraries - "es5", - "es2015.collection", - "es2015.core", - "es2015", - "es2015.generator", - "es2015.iterable", - "es2015.promise", - "es2015.proxy", - "es2015.reflect", - "es2015.symbol", - "es2015.symbol.wellknown", - "es2016.array.include", - "es2016", - "es2017", - "es2017.intl", - "es2017.object", - "es2017.sharedmemory", - "es2017.string", - "es2017.typedarrays", - "es2018.asyncgenerator", - "es2018.asynciterable", - "es2018", - "es2018.intl", - "es2018.promise", - "es2018.regexp", - "es2019.array", - "es2019", - "es2019.object", - "es2019.string", - "es2019.symbol", - "es2020.bigint", - "es2020", - "es2020.date", - "es2020.intl", - "es2020.number", - "es2020.promise", - "es2020.sharedmemory", - "es2020.string", - "es2020.symbol.wellknown", - "es2021", - "es2021.intl", - "es2021.promise", - "es2021.string", - "es2021.weakref", - "es2022", - "es2022.array", - "es2022.error", - "es2022.intl", - "es2022.object", - "es2022.string", - "esnext", - "esnext.array", - "esnext.intl", - ]; + // ensure we invalidate the build properly. + for (_, path) in op_crate_libs.iter() { + println!("cargo:rerun-if-changed={}", path.display()); + } - let path_dts = cwd.join("dts"); - // ensure we invalidate the build properly. - for name in libs.iter() { - println!( - "cargo:rerun-if-changed={}", - path_dts.join(format!("lib.{}.d.ts", name)).display() - ); - } + // libs that should be loaded into the isolate before snapshotting. + let libs = vec![ + // Deno custom type libraries + "deno.window", + "deno.worker", + "deno.shared_globals", + "deno.ns", + "deno.unstable", + // Deno built-in type libraries + "es5", + "es2015.collection", + "es2015.core", + "es2015", + "es2015.generator", + "es2015.iterable", + "es2015.promise", + "es2015.proxy", + "es2015.reflect", + "es2015.symbol", + "es2015.symbol.wellknown", + "es2016.array.include", + "es2016", + "es2017", + "es2017.intl", + "es2017.object", + "es2017.sharedmemory", + "es2017.string", + "es2017.typedarrays", + "es2018.asyncgenerator", + "es2018.asynciterable", + "es2018", + "es2018.intl", + "es2018.promise", + "es2018.regexp", + "es2019.array", + "es2019", + "es2019.object", + "es2019.string", + "es2019.symbol", + "es2020.bigint", + "es2020", + "es2020.date", + "es2020.intl", + "es2020.number", + "es2020.promise", + "es2020.sharedmemory", + "es2020.string", + "es2020.symbol.wellknown", + "es2021", + "es2021.intl", + "es2021.promise", + "es2021.string", + "es2021.weakref", + "es2022", + "es2022.array", + "es2022.error", + "es2022.intl", + "es2022.object", + "es2022.string", + "esnext", + "esnext.array", + "esnext.intl", + ]; + + let path_dts = cwd.join("dts"); + // ensure we invalidate the build properly. + for name in libs.iter() { + println!( + "cargo:rerun-if-changed={}", + path_dts.join(format!("lib.{}.d.ts", name)).display() + ); + } - // create a copy of the vector that includes any op crate libs to be passed - // to the JavaScript compiler to build into the snapshot - let mut build_libs = libs.clone(); - for (op_lib, _) in op_crate_libs.iter() { - build_libs.push(op_lib.to_owned()); - } + // create a copy of the vector that includes any op crate libs to be passed + // to the JavaScript compiler to build into the snapshot + let mut build_libs = libs.clone(); + for (op_lib, _) in op_crate_libs.iter() { + build_libs.push(op_lib.to_owned()); + } - #[op] - fn op_build_info(state: &mut OpState) -> Value { - let build_specifier = "asset:///bootstrap.ts"; - let build_libs = state.borrow::<Vec<&str>>(); - json!({ - "buildSpecifier": build_specifier, - "libs": build_libs, - }) - } + #[op] + fn op_build_info(state: &mut OpState) -> Value { + let build_specifier = "asset:///bootstrap.ts"; + let build_libs = state.borrow::<Vec<&str>>(); + json!({ + "buildSpecifier": build_specifier, + "libs": build_libs, + }) + } - #[op] - fn op_cwd() -> String { - "cache:///".into() - } + #[op] + fn op_cwd() -> String { + "cache:///".into() + } - #[op] - fn op_exists() -> bool { - false - } + #[op] + fn op_exists() -> bool { + false + } - #[op] - fn op_is_node_file() -> bool { - false - } + #[op] + fn op_is_node_file() -> bool { + false + } - #[op] - fn op_script_version( - _state: &mut OpState, - _args: Value, - ) -> Result<Option<String>, AnyError> { - Ok(Some("1".to_string())) - } + #[op] + fn op_script_version( + _state: &mut OpState, + _args: Value, + ) -> Result<Option<String>, AnyError> { + Ok(Some("1".to_string())) + } - #[op] - // using the same op that is used in `tsc.rs` for loading modules and reading - // files, but a slightly different implementation at build time. - fn op_load(state: &mut OpState, args: LoadArgs) -> Result<Value, AnyError> { - let op_crate_libs = state.borrow::<HashMap<&str, PathBuf>>(); - let path_dts = state.borrow::<PathBuf>(); - let re_asset = - Regex::new(r"asset:/{3}lib\.(\S+)\.d\.ts").expect("bad regex"); - let build_specifier = "asset:///bootstrap.ts"; - - // we need a basic file to send to tsc to warm it up. - if args.specifier == build_specifier { - Ok(json!({ - "data": r#"console.log("hello deno!");"#, - "version": "1", - // this corresponds to `ts.ScriptKind.TypeScript` - "scriptKind": 3 - })) - // specifiers come across as `asset:///lib.{lib_name}.d.ts` and we need to - // parse out just the name so we can lookup the asset. - } else if let Some(caps) = re_asset.captures(&args.specifier) { - if let Some(lib) = caps.get(1).map(|m| m.as_str()) { - // if it comes from an op crate, we were supplied with the path to the - // file. - let path = if let Some(op_crate_lib) = op_crate_libs.get(lib) { - PathBuf::from(op_crate_lib).canonicalize().unwrap() - // otherwise we are will generate the path ourself - } else { - path_dts.join(format!("lib.{}.d.ts", lib)) - }; - let data = std::fs::read_to_string(path)?; + #[op] + // using the same op that is used in `tsc.rs` for loading modules and reading + // files, but a slightly different implementation at build time. + fn op_load(state: &mut OpState, args: LoadArgs) -> Result<Value, AnyError> { + let op_crate_libs = state.borrow::<HashMap<&str, PathBuf>>(); + let path_dts = state.borrow::<PathBuf>(); + let re_asset = + Regex::new(r"asset:/{3}lib\.(\S+)\.d\.ts").expect("bad regex"); + let build_specifier = "asset:///bootstrap.ts"; + + // we need a basic file to send to tsc to warm it up. + if args.specifier == build_specifier { Ok(json!({ - "data": data, + "data": r#"console.log("hello deno!");"#, "version": "1", // this corresponds to `ts.ScriptKind.TypeScript` "scriptKind": 3 })) + // specifiers come across as `asset:///lib.{lib_name}.d.ts` and we need to + // parse out just the name so we can lookup the asset. + } else if let Some(caps) = re_asset.captures(&args.specifier) { + if let Some(lib) = caps.get(1).map(|m| m.as_str()) { + // if it comes from an op crate, we were supplied with the path to the + // file. + let path = if let Some(op_crate_lib) = op_crate_libs.get(lib) { + PathBuf::from(op_crate_lib).canonicalize().unwrap() + // otherwise we are will generate the path ourself + } else { + path_dts.join(format!("lib.{}.d.ts", lib)) + }; + let data = std::fs::read_to_string(path)?; + Ok(json!({ + "data": data, + "version": "1", + // this corresponds to `ts.ScriptKind.TypeScript` + "scriptKind": 3 + })) + } else { + Err(custom_error( + "InvalidSpecifier", + format!("An invalid specifier was requested: {}", args.specifier), + )) + } } else { Err(custom_error( "InvalidSpecifier", format!("An invalid specifier was requested: {}", args.specifier), )) } - } else { - Err(custom_error( - "InvalidSpecifier", - format!("An invalid specifier was requested: {}", args.specifier), - )) } + + create_snapshot(CreateSnapshotOptions { + cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), + snapshot_path, + startup_snapshot: None, + extensions: vec![Extension::builder() + .ops(vec![ + op_build_info::decl(), + op_cwd::decl(), + op_exists::decl(), + op_is_node_file::decl(), + op_load::decl(), + op_script_version::decl(), + ]) + .state(move |state| { + state.put(op_crate_libs.clone()); + state.put(build_libs.clone()); + state.put(path_dts.clone()); + + Ok(()) + }) + .build()], + additional_files: files, + compression_cb: Some(Box::new(|vec, snapshot_slice| { + vec.extend_from_slice( + &zstd::bulk::compress(snapshot_slice, 22) + .expect("snapshot compression failed"), + ); + })), + }); } - let js_runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - extensions: vec![Extension::builder() - .ops(vec![ - op_build_info::decl(), - op_cwd::decl(), - op_exists::decl(), - op_is_node_file::decl(), - op_load::decl(), - op_script_version::decl(), - ]) - .state(move |state| { - state.put(op_crate_libs.clone()); - state.put(build_libs.clone()); - state.put(path_dts.clone()); - - Ok(()) - }) - .build()], - ..Default::default() - }); - create_snapshot(js_runtime, snapshot_path, files); + pub(crate) fn version() -> String { + std::fs::read_to_string("tsc/00_typescript.js") + .unwrap() + .lines() + .find(|l| l.contains("ts.version = ")) + .expect( + "Failed to find the pattern `ts.version = ` in typescript source code", + ) + .chars() + .skip_while(|c| !char::is_numeric(*c)) + .take_while(|c| *c != '"') + .collect::<String>() + } } -fn ts_version() -> String { - std::fs::read_to_string("tsc/00_typescript.js") - .unwrap() - .lines() - .find(|l| l.contains("ts.version = ")) - .expect( - "Failed to find the pattern `ts.version = ` in typescript source code", - ) - .chars() - .skip_while(|c| !char::is_numeric(*c)) - .take_while(|c| *c != '"') - .collect::<String>() +fn create_cli_snapshot(snapshot_path: PathBuf, files: Vec<PathBuf>) { + let extensions: Vec<Extension> = vec![ + deno_webidl::init(), + deno_console::init(), + deno_url::init(), + deno_tls::init(), + deno_web::init::<Permissions>( + deno_web::BlobStore::default(), + Default::default(), + ), + deno_fetch::init::<Permissions>(Default::default()), + deno_cache::init::<SqliteBackedCache>(None), + deno_websocket::init::<Permissions>("".to_owned(), None, None), + deno_webstorage::init(None), + deno_crypto::init(None), + deno_webgpu::init(false), + deno_broadcast_channel::init( + deno_broadcast_channel::InMemoryBroadcastChannel::default(), + false, // No --unstable. + ), + deno_node::init::<Permissions>(None), // No --unstable. + deno_ffi::init::<Permissions>(false), + deno_net::init::<Permissions>( + None, false, // No --unstable. + None, + ), + deno_napi::init::<Permissions>(false), + deno_http::init(), + deno_flash::init::<Permissions>(false), // No --unstable + ]; + + create_snapshot(CreateSnapshotOptions { + cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), + snapshot_path, + startup_snapshot: Some(deno_runtime::js::deno_isolate_init()), + extensions, + additional_files: files, + compression_cb: Some(Box::new(|vec, snapshot_slice| { + lzzzz::lz4_hc::compress_to_vec( + snapshot_slice, + vec, + lzzzz::lz4_hc::CLEVEL_MAX, + ) + .expect("snapshot compression failed"); + })), + }) } fn git_commit_hash() -> String { @@ -386,7 +399,7 @@ fn main() { println!("cargo:rustc-env=GIT_COMMIT_HASH={}", git_commit_hash()); println!("cargo:rerun-if-env-changed=GIT_COMMIT_HASH"); - println!("cargo:rustc-env=TS_VERSION={}", ts_version()); + println!("cargo:rustc-env=TS_VERSION={}", ts::version()); println!("cargo:rerun-if-env-changed=TS_VERSION"); println!( @@ -440,11 +453,14 @@ fn main() { let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let o = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - // Main snapshot let compiler_snapshot_path = o.join("COMPILER_SNAPSHOT.bin"); + let js_files = get_js_files(env!("CARGO_MANIFEST_DIR"), "tsc"); + ts::create_compiler_snapshot(compiler_snapshot_path, js_files, &c); - let js_files = get_js_files("tsc"); - create_compiler_snapshot(&compiler_snapshot_path, js_files, &c); + let cli_snapshot_path = o.join("CLI_SNAPSHOT.bin"); + let mut js_files = get_js_files(env!("CARGO_MANIFEST_DIR"), "js"); + js_files.push(deno_runtime::js::get_99_main()); + create_cli_snapshot(cli_snapshot_path, js_files); #[cfg(target_os = "windows")] { @@ -462,17 +478,3 @@ fn deno_webgpu_get_declaration() -> PathBuf { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); manifest_dir.join("dts").join("lib.deno_webgpu.d.ts") } - -fn get_js_files(d: &str) -> Vec<PathBuf> { - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let mut js_files = std::fs::read_dir(d) - .unwrap() - .map(|dir_entry| { - let file = dir_entry.unwrap(); - manifest_dir.join(file.path()) - }) - .filter(|path| path.extension().unwrap_or_default() == "js") - .collect::<Vec<PathBuf>>(); - js_files.sort(); - js_files -} diff --git a/cli/js.rs b/cli/js.rs new file mode 100644 index 000000000..4bf3da627 --- /dev/null +++ b/cli/js.rs @@ -0,0 +1,57 @@ +use deno_core::Snapshot; +use log::debug; +use once_cell::sync::Lazy; + +pub static CLI_SNAPSHOT: Lazy<Box<[u8]>> = Lazy::new( + #[allow(clippy::uninit_vec)] + #[cold] + #[inline(never)] + || { + static COMPRESSED_CLI_SNAPSHOT: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin")); + + let size = + u32::from_le_bytes(COMPRESSED_CLI_SNAPSHOT[0..4].try_into().unwrap()) + as usize; + let mut vec = Vec::with_capacity(size); + + // SAFETY: vec is allocated with exact snapshot size (+ alignment) + // SAFETY: non zeroed bytes are overwritten with decompressed snapshot + unsafe { + vec.set_len(size); + } + + lzzzz::lz4::decompress(&COMPRESSED_CLI_SNAPSHOT[4..], &mut vec).unwrap(); + + vec.into_boxed_slice() + }, +); + +pub fn deno_isolate_init() -> Snapshot { + debug!("Deno isolate init with snapshots."); + Snapshot::Static(&CLI_SNAPSHOT) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn runtime_snapshot() { + let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions { + startup_snapshot: Some(deno_isolate_init()), + ..Default::default() + }); + js_runtime + .execute_script( + "<anon>", + r#" + if (!(bootstrap.mainRuntime && bootstrap.workerRuntime)) { + throw Error("bad"); + } + console.log("we have console.log!!!"); + "#, + ) + .unwrap(); + } +} diff --git a/cli/js/40_testing.js b/cli/js/40_testing.js new file mode 100644 index 000000000..864d50104 --- /dev/null +++ b/cli/js/40_testing.js @@ -0,0 +1,1412 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const core = window.Deno.core; + const ops = core.ops; + const { setExitHandler } = window.__bootstrap.os; + const { Console } = window.__bootstrap.console; + const { serializePermissions } = window.__bootstrap.permissions; + const { assert } = window.__bootstrap.infra; + const { + ArrayFrom, + ArrayPrototypeFilter, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeShift, + ArrayPrototypeSort, + BigInt, + DateNow, + Error, + FunctionPrototype, + Map, + MapPrototypeGet, + MapPrototypeHas, + MapPrototypeSet, + MathCeil, + ObjectKeys, + ObjectPrototypeIsPrototypeOf, + Promise, + SafeArrayIterator, + Set, + SymbolToStringTag, + TypeError, + } = window.__bootstrap.primordials; + + const opSanitizerDelayResolveQueue = []; + + // Even if every resource is closed by the end of a test, there can be a delay + // until the pending ops have all finished. This function returns a promise + // that resolves when it's (probably) fine to run the op sanitizer. + // + // This is implemented by adding a macrotask callback that runs after the + // timer macrotasks, so we can guarantee that a currently running interval + // will have an associated op. An additional `setTimeout` of 0 is needed + // before that, though, in order to give time for worker message ops to finish + // (since timeouts of 0 don't queue tasks in the timer queue immediately). + function opSanitizerDelay() { + return new Promise((resolve) => { + setTimeout(() => { + ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve); + }, 0); + }); + } + + function handleOpSanitizerDelayMacrotask() { + ArrayPrototypeShift(opSanitizerDelayResolveQueue)?.(); + return opSanitizerDelayResolveQueue.length === 0; + } + + // An async operation to $0 was started in this test, but never completed. This is often caused by not $1. + // An async operation to $0 was started in this test, but never completed. Async operations should not complete in a test if they were not started in that test. + // deno-fmt-ignore + const OP_DETAILS = { + "op_blob_read_part": ["read from a Blob or File", "awaiting the result of a Blob or File read"], + "op_broadcast_recv": ["receive a message from a BroadcastChannel", "closing the BroadcastChannel"], + "op_broadcast_send": ["send a message to a BroadcastChannel", "closing the BroadcastChannel"], + "op_chmod_async": ["change the permissions of a file", "awaiting the result of a `Deno.chmod` call"], + "op_chown_async": ["change the owner of a file", "awaiting the result of a `Deno.chown` call"], + "op_copy_file_async": ["copy a file", "awaiting the result of a `Deno.copyFile` call"], + "op_crypto_decrypt": ["decrypt data", "awaiting the result of a `crypto.subtle.decrypt` call"], + "op_crypto_derive_bits": ["derive bits from a key", "awaiting the result of a `crypto.subtle.deriveBits` call"], + "op_crypto_encrypt": ["encrypt data", "awaiting the result of a `crypto.subtle.encrypt` call"], + "op_crypto_generate_key": ["generate a key", "awaiting the result of a `crypto.subtle.generateKey` call"], + "op_crypto_sign_key": ["sign data", "awaiting the result of a `crypto.subtle.sign` call"], + "op_crypto_subtle_digest": ["digest data", "awaiting the result of a `crypto.subtle.digest` call"], + "op_crypto_verify_key": ["verify data", "awaiting the result of a `crypto.subtle.verify` call"], + "op_net_recv_udp": ["receive a datagram message via UDP", "awaiting the result of `Deno.DatagramConn#receive` call, or not breaking out of a for await loop looping over a `Deno.DatagramConn`"], + "op_net_recv_unixpacket": ["receive a datagram message via Unixpacket", "awaiting the result of `Deno.DatagramConn#receive` call, or not breaking out of a for await loop looping over a `Deno.DatagramConn`"], + "op_net_send_udp": ["send a datagram message via UDP", "awaiting the result of `Deno.DatagramConn#send` call"], + "op_net_send_unixpacket": ["send a datagram message via Unixpacket", "awaiting the result of `Deno.DatagramConn#send` call"], + "op_dns_resolve": ["resolve a DNS name", "awaiting the result of a `Deno.resolveDns` call"], + "op_fdatasync_async": ["flush pending data operations for a file to disk", "awaiting the result of a `Deno.fdatasync` call"], + "op_fetch_send": ["send a HTTP request", "awaiting the result of a `fetch` call"], + "op_ffi_call_nonblocking": ["do a non blocking ffi call", "awaiting the returned promise"] , + "op_ffi_call_ptr_nonblocking": ["do a non blocking ffi call", "awaiting the returned promise"], + "op_flock_async": ["lock a file", "awaiting the result of a `Deno.flock` call"], + "op_fs_events_poll": ["get the next file system event", "breaking out of a for await loop looping over `Deno.FsEvents`"], + "op_fstat_async": ["get file metadata", "awaiting the result of a `Deno.File#fstat` call"], + "op_fsync_async": ["flush pending data operations for a file to disk", "awaiting the result of a `Deno.fsync` call"], + "op_ftruncate_async": ["truncate a file", "awaiting the result of a `Deno.ftruncate` call"], + "op_funlock_async": ["unlock a file", "awaiting the result of a `Deno.funlock` call"], + "op_futime_async": ["change file timestamps", "awaiting the result of a `Deno.futime` call"], + "op_http_accept": ["accept a HTTP request", "closing a `Deno.HttpConn`"], + "op_http_shutdown": ["shutdown a HTTP connection", "awaiting `Deno.HttpEvent#respondWith`"], + "op_http_upgrade_websocket": ["upgrade a HTTP connection to a WebSocket", "awaiting `Deno.HttpEvent#respondWith`"], + "op_http_write_headers": ["write HTTP response headers", "awaiting `Deno.HttpEvent#respondWith`"], + "op_http_write": ["write HTTP response body", "awaiting `Deno.HttpEvent#respondWith`"], + "op_link_async": ["create a hard link", "awaiting the result of a `Deno.link` call"], + "op_make_temp_dir_async": ["create a temporary directory", "awaiting the result of a `Deno.makeTempDir` call"], + "op_make_temp_file_async": ["create a temporary file", "awaiting the result of a `Deno.makeTempFile` call"], + "op_message_port_recv_message": ["receive a message from a MessagePort", "awaiting the result of not closing a `MessagePort`"], + "op_mkdir_async": ["create a directory", "awaiting the result of a `Deno.mkdir` call"], + "op_net_accept_tcp": ["accept a TCP stream", "closing a `Deno.Listener`"], + "op_net_accept_unix": ["accept a Unix stream", "closing a `Deno.Listener`"], + "op_net_connect_tcp": ["connect to a TCP server", "awaiting a `Deno.connect` call"], + "op_net_connect_unix": ["connect to a Unix server", "awaiting a `Deno.connect` call"], + "op_open_async": ["open a file", "awaiting the result of a `Deno.open` call"], + "op_read_dir_async": ["read a directory", "collecting all items in the async iterable returned from a `Deno.readDir` call"], + "op_read_link_async": ["read a symlink", "awaiting the result of a `Deno.readLink` call"], + "op_realpath_async": ["resolve a path", "awaiting the result of a `Deno.realpath` call"], + "op_remove_async": ["remove a file or directory", "awaiting the result of a `Deno.remove` call"], + "op_rename_async": ["rename a file or directory", "awaiting the result of a `Deno.rename` call"], + "op_run_status": ["get the status of a subprocess", "awaiting the result of a `Deno.Process#status` call"], + "op_seek_async": ["seek in a file", "awaiting the result of a `Deno.File#seek` call"], + "op_signal_poll": ["get the next signal", "un-registering a OS signal handler"], + "op_sleep": ["sleep for a duration", "cancelling a `setTimeout` or `setInterval` call"], + "op_stat_async": ["get file metadata", "awaiting the result of a `Deno.stat` call"], + "op_symlink_async": ["create a symlink", "awaiting the result of a `Deno.symlink` call"], + "op_net_accept_tls": ["accept a TLS stream", "closing a `Deno.TlsListener`"], + "op_net_connect_tls": ["connect to a TLS server", "awaiting a `Deno.connectTls` call"], + "op_tls_handshake": ["perform a TLS handshake", "awaiting a `Deno.TlsConn#handshake` call"], + "op_tls_start": ["start a TLS connection", "awaiting a `Deno.startTls` call"], + "op_truncate_async": ["truncate a file", "awaiting the result of a `Deno.truncate` call"], + "op_utime_async": ["change file timestamps", "awaiting the result of a `Deno.utime` call"], + "op_webgpu_buffer_get_map_async": ["map a WebGPU buffer", "awaiting the result of a `GPUBuffer#mapAsync` call"], + "op_webgpu_request_adapter": ["request a WebGPU adapter", "awaiting the result of a `navigator.gpu.requestAdapter` call"], + "op_webgpu_request_device": ["request a WebGPU device", "awaiting the result of a `GPUAdapter#requestDevice` call"], + "op_worker_recv_message": ["receive a message from a web worker", "terminating a `Worker`"], + "op_ws_close": ["close a WebSocket", "awaiting until the `close` event is emitted on a `WebSocket`, or the `WebSocketStream#closed` promise resolves"], + "op_ws_create": ["create a WebSocket", "awaiting until the `open` event is emitted on a `WebSocket`, or the result of a `WebSocketStream#connection` promise"], + "op_ws_next_event": ["receive the next message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], + "op_ws_send": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], + }; + + // Wrap test function in additional assertion that makes sure + // the test case does not leak async "ops" - ie. number of async + // completed ops after the test is the same as number of dispatched + // ops. Note that "unref" ops are ignored since in nature that are + // optional. + function assertOps(fn) { + /** @param desc {TestDescription | TestStepDescription} */ + return async function asyncOpSanitizer(desc) { + const pre = core.metrics(); + const preTraces = new Map(core.opCallTraces); + try { + await fn(desc); + } finally { + // Defer until next event loop turn - that way timeouts and intervals + // cleared can actually be removed from resource table, otherwise + // false positives may occur (https://github.com/denoland/deno/issues/4591) + await opSanitizerDelay(); + await opSanitizerDelay(); + } + + if (shouldSkipSanitizers(desc)) return; + + const post = core.metrics(); + const postTraces = new Map(core.opCallTraces); + + // We're checking diff because one might spawn HTTP server in the background + // that will be a pending async op before test starts. + const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync; + const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync; + + if (dispatchedDiff === completedDiff) return; + + const details = []; + for (const key in post.ops) { + const preOp = pre.ops[key] ?? + { opsDispatchedAsync: 0, opsCompletedAsync: 0 }; + const postOp = post.ops[key]; + const dispatchedDiff = postOp.opsDispatchedAsync - + preOp.opsDispatchedAsync; + const completedDiff = postOp.opsCompletedAsync - + preOp.opsCompletedAsync; + + if (dispatchedDiff > completedDiff) { + const [name, hint] = OP_DETAILS[key] || [key, null]; + const count = dispatchedDiff - completedDiff; + let message = `${count} async operation${ + count === 1 ? "" : "s" + } to ${name} ${ + count === 1 ? "was" : "were" + } started in this test, but never completed.`; + if (hint) { + message += ` This is often caused by not ${hint}.`; + } + const traces = []; + for (const [id, { opName, stack }] of postTraces) { + if (opName !== key) continue; + if (MapPrototypeHas(preTraces, id)) continue; + ArrayPrototypePush(traces, stack); + } + if (traces.length === 1) { + message += " The operation was started here:\n"; + message += traces[0]; + } else if (traces.length > 1) { + message += " The operations were started here:\n"; + message += ArrayPrototypeJoin(traces, "\n\n"); + } + ArrayPrototypePush(details, message); + } else if (dispatchedDiff < completedDiff) { + const [name, hint] = OP_DETAILS[key] || [key, null]; + const count = completedDiff - dispatchedDiff; + ArrayPrototypePush( + details, + `${count} async operation${count === 1 ? "" : "s"} to ${name} ${ + count === 1 ? "was" : "were" + } started before this test, but ${ + count === 1 ? "was" : "were" + } completed during the test. Async operations should not complete in a test if they were not started in that test. + ${hint ? `This is often caused by not ${hint}.` : ""}`, + ); + } + } + + let msg = `Test case is leaking async ops. + + - ${ArrayPrototypeJoin(details, "\n - ")}`; + + if (!core.isOpCallTracingEnabled()) { + msg += + `\n\nTo get more details where ops were leaked, run again with --trace-ops flag.`; + } else { + msg += "\n"; + } + + throw assert(false, msg); + }; + } + + function prettyResourceNames(name) { + switch (name) { + case "fsFile": + return ["A file", "opened", "closed"]; + case "fetchRequest": + return ["A fetch request", "started", "finished"]; + case "fetchRequestBody": + return ["A fetch request body", "created", "closed"]; + case "fetchResponseBody": + return ["A fetch response body", "created", "consumed"]; + case "httpClient": + return ["An HTTP client", "created", "closed"]; + case "dynamicLibrary": + return ["A dynamic library", "loaded", "unloaded"]; + case "httpConn": + return ["An inbound HTTP connection", "accepted", "closed"]; + case "httpStream": + return ["An inbound HTTP request", "accepted", "closed"]; + case "tcpStream": + return ["A TCP connection", "opened/accepted", "closed"]; + case "unixStream": + return ["A Unix connection", "opened/accepted", "closed"]; + case "tlsStream": + return ["A TLS connection", "opened/accepted", "closed"]; + case "tlsListener": + return ["A TLS listener", "opened", "closed"]; + case "unixListener": + return ["A Unix listener", "opened", "closed"]; + case "unixDatagram": + return ["A Unix datagram", "opened", "closed"]; + case "tcpListener": + return ["A TCP listener", "opened", "closed"]; + case "udpSocket": + return ["A UDP socket", "opened", "closed"]; + case "timer": + return ["A timer", "started", "fired/cleared"]; + case "textDecoder": + return ["A text decoder", "created", "finished"]; + case "messagePort": + return ["A message port", "created", "closed"]; + case "webSocketStream": + return ["A WebSocket", "opened", "closed"]; + case "fsEvents": + return ["A file system watcher", "created", "closed"]; + case "childStdin": + return ["A child process stdin", "opened", "closed"]; + case "childStdout": + return ["A child process stdout", "opened", "closed"]; + case "childStderr": + return ["A child process stderr", "opened", "closed"]; + case "child": + return ["A child process", "started", "closed"]; + case "signal": + return ["A signal listener", "created", "fired/cleared"]; + case "stdin": + return ["The stdin pipe", "opened", "closed"]; + case "stdout": + return ["The stdout pipe", "opened", "closed"]; + case "stderr": + return ["The stderr pipe", "opened", "closed"]; + case "compression": + return ["A CompressionStream", "created", "closed"]; + default: + return [`A "${name}" resource`, "created", "cleaned up"]; + } + } + + function resourceCloseHint(name) { + switch (name) { + case "fsFile": + return "Close the file handle by calling `file.close()`."; + case "fetchRequest": + return "Await the promise returned from `fetch()` or abort the fetch with an abort signal."; + case "fetchRequestBody": + return "Terminate the request body `ReadableStream` by closing or erroring it."; + case "fetchResponseBody": + return "Consume or close the response body `ReadableStream`, e.g `await resp.text()` or `await resp.body.cancel()`."; + case "httpClient": + return "Close the HTTP client by calling `httpClient.close()`."; + case "dynamicLibrary": + return "Unload the dynamic library by calling `dynamicLibrary.close()`."; + case "httpConn": + return "Close the inbound HTTP connection by calling `httpConn.close()`."; + case "httpStream": + return "Close the inbound HTTP request by responding with `e.respondWith().` or closing the HTTP connection."; + case "tcpStream": + return "Close the TCP connection by calling `tcpConn.close()`."; + case "unixStream": + return "Close the Unix socket connection by calling `unixConn.close()`."; + case "tlsStream": + return "Close the TLS connection by calling `tlsConn.close()`."; + case "tlsListener": + return "Close the TLS listener by calling `tlsListener.close()`."; + case "unixListener": + return "Close the Unix socket listener by calling `unixListener.close()`."; + case "unixDatagram": + return "Close the Unix datagram socket by calling `unixDatagram.close()`."; + case "tcpListener": + return "Close the TCP listener by calling `tcpListener.close()`."; + case "udpSocket": + return "Close the UDP socket by calling `udpSocket.close()`."; + case "timer": + return "Clear the timer by calling `clearInterval` or `clearTimeout`."; + case "textDecoder": + return "Close the text decoder by calling `textDecoder.decode('')` or `await textDecoderStream.readable.cancel()`."; + case "messagePort": + return "Close the message port by calling `messagePort.close()`."; + case "webSocketStream": + return "Close the WebSocket by calling `webSocket.close()`."; + case "fsEvents": + return "Close the file system watcher by calling `watcher.close()`."; + case "childStdin": + return "Close the child process stdin by calling `proc.stdin.close()`."; + case "childStdout": + return "Close the child process stdout by calling `proc.stdout.close()`."; + case "childStderr": + return "Close the child process stderr by calling `proc.stderr.close()`."; + case "child": + return "Close the child process by calling `proc.kill()` or `proc.close()`."; + case "signal": + return "Clear the signal listener by calling `Deno.removeSignalListener`."; + case "stdin": + return "Close the stdin pipe by calling `Deno.stdin.close()`."; + case "stdout": + return "Close the stdout pipe by calling `Deno.stdout.close()`."; + case "stderr": + return "Close the stderr pipe by calling `Deno.stderr.close()`."; + case "compression": + return "Close the compression stream by calling `await stream.writable.close()`."; + default: + return "Close the resource before the end of the test."; + } + } + + // Wrap test function in additional assertion that makes sure + // the test case does not "leak" resources - ie. resource table after + // the test has exactly the same contents as before the test. + function assertResources(fn) { + /** @param desc {TestDescription | TestStepDescription} */ + return async function resourceSanitizer(desc) { + const pre = core.resources(); + await fn(desc); + + if (shouldSkipSanitizers(desc)) { + return; + } + + const post = core.resources(); + + const allResources = new Set([ + ...new SafeArrayIterator(ObjectKeys(pre)), + ...new SafeArrayIterator(ObjectKeys(post)), + ]); + + const details = []; + for (const resource of allResources) { + const preResource = pre[resource]; + const postResource = post[resource]; + if (preResource === postResource) continue; + + if (preResource === undefined) { + const [name, action1, action2] = prettyResourceNames(postResource); + const hint = resourceCloseHint(postResource); + const detail = + `${name} (rid ${resource}) was ${action1} during the test, but not ${action2} during the test. ${hint}`; + ArrayPrototypePush(details, detail); + } else { + const [name, action1, action2] = prettyResourceNames(preResource); + const detail = + `${name} (rid ${resource}) was ${action1} before the test started, but was ${action2} during the test. Do not close resources in a test that were not created during that test.`; + ArrayPrototypePush(details, detail); + } + } + + const message = `Test case is leaking ${details.length} resource${ + details.length === 1 ? "" : "s" + }: + + - ${details.join("\n - ")} +`; + assert(details.length === 0, message); + }; + } + + // Wrap test function in additional assertion that makes sure + // that the test case does not accidentally exit prematurely. + function assertExit(fn, isTest) { + return async function exitSanitizer(...params) { + setExitHandler((exitCode) => { + assert( + false, + `${ + isTest ? "Test case" : "Bench" + } attempted to exit with exit code: ${exitCode}`, + ); + }); + + try { + await fn(...new SafeArrayIterator(params)); + } catch (err) { + throw err; + } finally { + setExitHandler(null); + } + }; + } + + function assertTestStepScopes(fn) { + /** @param desc {TestDescription | TestStepDescription} */ + return async function testStepSanitizer(desc) { + preValidation(); + // only report waiting after pre-validation + if (canStreamReporting(desc) && "parent" in desc) { + stepReportWait(desc); + } + await fn(MapPrototypeGet(testStates, desc.id).context); + testStepPostValidation(desc); + + function preValidation() { + const runningStepDescs = getRunningStepDescs(); + const runningStepDescsWithSanitizers = ArrayPrototypeFilter( + runningStepDescs, + (d) => usesSanitizer(d), + ); + + if (runningStepDescsWithSanitizers.length > 0) { + throw new Error( + "Cannot start test step while another test step with sanitizers is running.\n" + + runningStepDescsWithSanitizers + .map((d) => ` * ${getFullName(d)}`) + .join("\n"), + ); + } + + if (usesSanitizer(desc) && runningStepDescs.length > 0) { + throw new Error( + "Cannot start test step with sanitizers while another test step is running.\n" + + runningStepDescs.map((d) => ` * ${getFullName(d)}`).join("\n"), + ); + } + + function getRunningStepDescs() { + const results = []; + let childDesc = desc; + while (childDesc.parent != null) { + const state = MapPrototypeGet(testStates, childDesc.parent.id); + for (const siblingDesc of state.children) { + if (siblingDesc.id == childDesc.id) { + continue; + } + const siblingState = MapPrototypeGet(testStates, siblingDesc.id); + if (!siblingState.finalized) { + ArrayPrototypePush(results, siblingDesc); + } + } + childDesc = childDesc.parent; + } + return results; + } + } + }; + } + + function testStepPostValidation(desc) { + // check for any running steps + for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { + if (MapPrototypeGet(testStates, childDesc.id).status == "pending") { + throw new Error( + "There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).", + ); + } + } + + // check if an ancestor already completed + let currentDesc = desc.parent; + while (currentDesc != null) { + if (MapPrototypeGet(testStates, currentDesc.id).finalized) { + throw new Error( + "Parent scope completed before test step finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).", + ); + } + currentDesc = currentDesc.parent; + } + } + + function pledgePermissions(permissions) { + return ops.op_pledge_test_permissions( + serializePermissions(permissions), + ); + } + + function restorePermissions(token) { + ops.op_restore_test_permissions(token); + } + + function withPermissions(fn, permissions) { + return async function applyPermissions(...params) { + const token = pledgePermissions(permissions); + + try { + await fn(...new SafeArrayIterator(params)); + } finally { + restorePermissions(token); + } + }; + } + + /** + * @typedef {{ + * id: number, + * name: string, + * fn: TestFunction + * origin: string, + * location: TestLocation, + * filteredOut: boolean, + * ignore: boolean, + * only: boolean. + * sanitizeOps: boolean, + * sanitizeResources: boolean, + * sanitizeExit: boolean, + * permissions: PermissionOptions, + * }} TestDescription + * + * @typedef {{ + * id: number, + * name: string, + * fn: TestFunction + * origin: string, + * location: TestLocation, + * ignore: boolean, + * level: number, + * parent: TestDescription | TestStepDescription, + * rootId: number, + * rootName: String, + * sanitizeOps: boolean, + * sanitizeResources: boolean, + * sanitizeExit: boolean, + * }} TestStepDescription + * + * @typedef {{ + * context: TestContext, + * children: TestStepDescription[], + * finalized: boolean, + * }} TestState + * + * @typedef {{ + * context: TestContext, + * children: TestStepDescription[], + * finalized: boolean, + * status: "pending" | "ok" | ""failed" | ignored", + * error: unknown, + * elapsed: number | null, + * reportedWait: boolean, + * reportedResult: boolean, + * }} TestStepState + * + * @typedef {{ + * id: number, + * name: string, + * fn: BenchFunction + * origin: string, + * filteredOut: boolean, + * ignore: boolean, + * only: boolean. + * sanitizeExit: boolean, + * permissions: PermissionOptions, + * }} BenchDescription + */ + + /** @type {TestDescription[]} */ + const testDescs = []; + /** @type {Map<number, TestState | TestStepState>} */ + const testStates = new Map(); + /** @type {BenchDescription[]} */ + const benchDescs = []; + let isTestSubcommand = false; + let isBenchSubcommand = false; + + // Main test function provided by Deno. + function test( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, + ) { + if (!isTestSubcommand) { + return; + } + + let testDesc; + const defaults = { + ignore: false, + only: false, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true, + permissions: null, + }; + + if (typeof nameOrFnOrOptions === "string") { + if (!nameOrFnOrOptions) { + throw new TypeError("The test name can't be empty"); + } + if (typeof optionsOrFn === "function") { + testDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; + } else { + if (!maybeFn || typeof maybeFn !== "function") { + throw new TypeError("Missing test function"); + } + if (optionsOrFn.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, test function is already provided as the third argument.", + ); + } + if (optionsOrFn.name != undefined) { + throw new TypeError( + "Unexpected 'name' field in options, test name is already provided as the first argument.", + ); + } + testDesc = { + ...defaults, + ...optionsOrFn, + fn: maybeFn, + name: nameOrFnOrOptions, + }; + } + } else if (typeof nameOrFnOrOptions === "function") { + if (!nameOrFnOrOptions.name) { + throw new TypeError("The test function must have a name"); + } + if (optionsOrFn != undefined) { + throw new TypeError("Unexpected second argument to Deno.test()"); + } + if (maybeFn != undefined) { + throw new TypeError("Unexpected third argument to Deno.test()"); + } + testDesc = { + ...defaults, + fn: nameOrFnOrOptions, + name: nameOrFnOrOptions.name, + }; + } else { + let fn; + let name; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + if (nameOrFnOrOptions.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, test function is already provided as the second argument.", + ); + } + name = nameOrFnOrOptions.name ?? fn.name; + } else { + if ( + !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" + ) { + throw new TypeError( + "Expected 'fn' field in the first argument to be a test function.", + ); + } + fn = nameOrFnOrOptions.fn; + name = nameOrFnOrOptions.name ?? fn.name; + } + if (!name) { + throw new TypeError("The test name can't be empty"); + } + testDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; + } + + // Delete this prop in case the user passed it. It's used to detect steps. + delete testDesc.parent; + testDesc.fn = wrapTestFnWithSanitizers(testDesc.fn, testDesc); + if (testDesc.permissions) { + testDesc.fn = withPermissions( + testDesc.fn, + testDesc.permissions, + ); + } + testDesc.origin = getTestOrigin(); + const jsError = Deno.core.destructureError(new Error()); + testDesc.location = { + fileName: jsError.frames[1].fileName, + lineNumber: jsError.frames[1].lineNumber, + columnNumber: jsError.frames[1].columnNumber, + }; + + const { id, filteredOut } = ops.op_register_test(testDesc); + testDesc.id = id; + testDesc.filteredOut = filteredOut; + + ArrayPrototypePush(testDescs, testDesc); + MapPrototypeSet(testStates, testDesc.id, { + context: createTestContext(testDesc), + children: [], + finalized: false, + }); + } + + // Main bench function provided by Deno. + function bench( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, + ) { + if (!isBenchSubcommand) { + return; + } + + let benchDesc; + const defaults = { + ignore: false, + baseline: false, + only: false, + sanitizeExit: true, + permissions: null, + }; + + if (typeof nameOrFnOrOptions === "string") { + if (!nameOrFnOrOptions) { + throw new TypeError("The bench name can't be empty"); + } + if (typeof optionsOrFn === "function") { + benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; + } else { + if (!maybeFn || typeof maybeFn !== "function") { + throw new TypeError("Missing bench function"); + } + if (optionsOrFn.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, bench function is already provided as the third argument.", + ); + } + if (optionsOrFn.name != undefined) { + throw new TypeError( + "Unexpected 'name' field in options, bench name is already provided as the first argument.", + ); + } + benchDesc = { + ...defaults, + ...optionsOrFn, + fn: maybeFn, + name: nameOrFnOrOptions, + }; + } + } else if (typeof nameOrFnOrOptions === "function") { + if (!nameOrFnOrOptions.name) { + throw new TypeError("The bench function must have a name"); + } + if (optionsOrFn != undefined) { + throw new TypeError("Unexpected second argument to Deno.bench()"); + } + if (maybeFn != undefined) { + throw new TypeError("Unexpected third argument to Deno.bench()"); + } + benchDesc = { + ...defaults, + fn: nameOrFnOrOptions, + name: nameOrFnOrOptions.name, + }; + } else { + let fn; + let name; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + if (nameOrFnOrOptions.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, bench function is already provided as the second argument.", + ); + } + name = nameOrFnOrOptions.name ?? fn.name; + } else { + if ( + !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" + ) { + throw new TypeError( + "Expected 'fn' field in the first argument to be a bench function.", + ); + } + fn = nameOrFnOrOptions.fn; + name = nameOrFnOrOptions.name ?? fn.name; + } + if (!name) { + throw new TypeError("The bench name can't be empty"); + } + benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; + } + + benchDesc.origin = getBenchOrigin(); + const AsyncFunction = (async () => {}).constructor; + benchDesc.async = AsyncFunction === benchDesc.fn.constructor; + + const { id, filteredOut } = ops.op_register_bench(benchDesc); + benchDesc.id = id; + benchDesc.filteredOut = filteredOut; + + ArrayPrototypePush(benchDescs, benchDesc); + } + + async function runTest(desc) { + if (desc.ignore) { + return "ignored"; + } + + try { + await desc.fn(desc); + const failCount = failedChildStepsCount(desc); + return failCount === 0 ? "ok" : { + "failed": core.destructureError( + new Error( + `${failCount} test step${failCount === 1 ? "" : "s"} failed.`, + ), + ), + }; + } catch (error) { + return { + "failed": core.destructureError(error), + }; + } finally { + const state = MapPrototypeGet(testStates, desc.id); + state.finalized = true; + // ensure the children report their result + for (const childDesc of state.children) { + stepReportResult(childDesc); + } + } + } + + function compareMeasurements(a, b) { + if (a > b) return 1; + if (a < b) return -1; + + return 0; + } + + function benchStats(n, highPrecision, avg, min, max, all) { + return { + n, + min, + max, + p75: all[MathCeil(n * (75 / 100)) - 1], + p99: all[MathCeil(n * (99 / 100)) - 1], + p995: all[MathCeil(n * (99.5 / 100)) - 1], + p999: all[MathCeil(n * (99.9 / 100)) - 1], + avg: !highPrecision ? (avg / n) : MathCeil(avg / n), + }; + } + + async function benchMeasure(timeBudget, desc) { + const fn = desc.fn; + let n = 0; + let avg = 0; + let wavg = 0; + const all = []; + let min = Infinity; + let max = -Infinity; + const lowPrecisionThresholdInNs = 1e4; + + // warmup step + let c = 0; + let iterations = 20; + let budget = 10 * 1e6; + + if (!desc.async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + + fn(); + const iterationTime = benchNow() - t1; + + c++; + wavg += iterationTime; + budget -= iterationTime; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + + await fn(); + const iterationTime = benchNow() - t1; + + c++; + wavg += iterationTime; + budget -= iterationTime; + } + } + + wavg /= c; + + // measure step + if (wavg > lowPrecisionThresholdInNs) { + let iterations = 10; + let budget = timeBudget * 1e6; + + if (!desc.async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + + fn(); + const iterationTime = benchNow() - t1; + + n++; + avg += iterationTime; + budget -= iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + + await fn(); + const iterationTime = benchNow() - t1; + + n++; + avg += iterationTime; + budget -= iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + } + } + } else { + let iterations = 10; + let budget = timeBudget * 1e6; + + if (!desc.async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + for (let c = 0; c < lowPrecisionThresholdInNs; c++) fn(); + const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; + + n++; + avg += iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + budget -= iterationTime * lowPrecisionThresholdInNs; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + for (let c = 0; c < lowPrecisionThresholdInNs; c++) await fn(); + const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; + + n++; + avg += iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + budget -= iterationTime * lowPrecisionThresholdInNs; + } + } + } + + all.sort(compareMeasurements); + return benchStats(n, wavg > lowPrecisionThresholdInNs, avg, min, max, all); + } + + async function runBench(desc) { + let token = null; + + try { + if (desc.permissions) { + token = pledgePermissions(desc.permissions); + } + + if (desc.sanitizeExit) { + setExitHandler((exitCode) => { + assert( + false, + `Bench attempted to exit with exit code: ${exitCode}`, + ); + }); + } + + const benchTimeInMs = 500; + const stats = await benchMeasure(benchTimeInMs, desc); + + return { ok: stats }; + } catch (error) { + return { failed: core.destructureError(error) }; + } finally { + if (bench.sanitizeExit) setExitHandler(null); + if (token !== null) restorePermissions(token); + } + } + + let origin = null; + + function getTestOrigin() { + if (origin == null) { + origin = ops.op_get_test_origin(); + } + return origin; + } + + function getBenchOrigin() { + if (origin == null) { + origin = ops.op_get_bench_origin(); + } + return origin; + } + + function benchNow() { + return ops.op_bench_now(); + } + + function enableTest() { + isTestSubcommand = true; + } + + function enableBench() { + isBenchSubcommand = true; + } + + async function runTests({ + shuffle = null, + } = {}) { + core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask); + + const origin = getTestOrigin(); + const only = ArrayPrototypeFilter(testDescs, (test) => test.only); + const filtered = ArrayPrototypeFilter( + only.length > 0 ? only : testDescs, + (desc) => !desc.filteredOut, + ); + + ops.op_dispatch_test_event({ + plan: { + origin, + total: filtered.length, + filteredOut: testDescs.length - filtered.length, + usedOnly: only.length > 0, + }, + }); + + if (shuffle !== null) { + // http://en.wikipedia.org/wiki/Linear_congruential_generator + // Use BigInt for everything because the random seed is u64. + const nextInt = (function (state) { + const m = 0x80000000n; + const a = 1103515245n; + const c = 12345n; + + return function (max) { + return state = ((a * state + c) % m) % BigInt(max); + }; + }(BigInt(shuffle))); + + for (let i = filtered.length - 1; i > 0; i--) { + const j = nextInt(i); + [filtered[i], filtered[j]] = [filtered[j], filtered[i]]; + } + } + + for (const desc of filtered) { + ops.op_dispatch_test_event({ wait: desc.id }); + const earlier = DateNow(); + const result = await runTest(desc); + const elapsed = DateNow() - earlier; + ops.op_dispatch_test_event({ + result: [desc.id, result, elapsed], + }); + } + } + + async function runBenchmarks() { + core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask); + + const origin = getBenchOrigin(); + const originalConsole = globalThis.console; + + globalThis.console = new Console((s) => { + ops.op_dispatch_bench_event({ output: s }); + }); + + const only = ArrayPrototypeFilter(benchDescs, (bench) => bench.only); + const filtered = ArrayPrototypeFilter( + only.length > 0 ? only : benchDescs, + (desc) => !desc.filteredOut && !desc.ignore, + ); + + let groups = new Set(); + // make sure ungrouped benchmarks are placed above grouped + groups.add(undefined); + + for (const desc of filtered) { + desc.group ||= undefined; + groups.add(desc.group); + } + + groups = ArrayFrom(groups); + ArrayPrototypeSort( + filtered, + (a, b) => groups.indexOf(a.group) - groups.indexOf(b.group), + ); + + ops.op_dispatch_bench_event({ + plan: { + origin, + total: filtered.length, + usedOnly: only.length > 0, + names: ArrayPrototypeMap(filtered, (desc) => desc.name), + }, + }); + + for (const desc of filtered) { + desc.baseline = !!desc.baseline; + ops.op_dispatch_bench_event({ wait: desc.id }); + ops.op_dispatch_bench_event({ + result: [desc.id, await runBench(desc)], + }); + } + + globalThis.console = originalConsole; + } + + function getFullName(desc) { + if ("parent" in desc) { + return `${desc.parent.name} > ${desc.name}`; + } + return desc.name; + } + + function usesSanitizer(desc) { + return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit; + } + + function canStreamReporting(desc) { + let currentDesc = desc; + while (currentDesc != null) { + if (!usesSanitizer(currentDesc)) { + return false; + } + currentDesc = currentDesc.parent; + } + for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { + const state = MapPrototypeGet(testStates, childDesc.id); + if (!usesSanitizer(childDesc) && !state.finalized) { + return false; + } + } + return true; + } + + function stepReportWait(desc) { + const state = MapPrototypeGet(testStates, desc.id); + if (state.reportedWait) { + return; + } + ops.op_dispatch_test_event({ stepWait: desc.id }); + state.reportedWait = true; + } + + function stepReportResult(desc) { + const state = MapPrototypeGet(testStates, desc.id); + if (state.reportedResult) { + return; + } + stepReportWait(desc); + for (const childDesc of state.children) { + stepReportResult(childDesc); + } + let result; + if (state.status == "pending" || state.status == "failed") { + result = { + [state.status]: state.error && core.destructureError(state.error), + }; + } else { + result = state.status; + } + ops.op_dispatch_test_event({ + stepResult: [desc.id, result, state.elapsed], + }); + state.reportedResult = true; + } + + function failedChildStepsCount(desc) { + return ArrayPrototypeFilter( + MapPrototypeGet(testStates, desc.id).children, + (d) => MapPrototypeGet(testStates, d.id).status === "failed", + ).length; + } + + /** If a test validation error already occurred then don't bother checking + * the sanitizers as that will create extra noise. + */ + function shouldSkipSanitizers(desc) { + try { + testStepPostValidation(desc); + return false; + } catch { + return true; + } + } + + /** @param desc {TestDescription | TestStepDescription} */ + function createTestContext(desc) { + let parent; + let level; + let rootId; + let rootName; + if ("parent" in desc) { + parent = MapPrototypeGet(testStates, desc.parent.id).context; + level = desc.level; + rootId = desc.rootId; + rootName = desc.rootName; + } else { + parent = undefined; + level = 0; + rootId = desc.id; + rootName = desc.name; + } + return { + [SymbolToStringTag]: "TestContext", + /** + * The current test name. + */ + name: desc.name, + /** + * Parent test context. + */ + parent, + /** + * File Uri of the test code. + */ + origin: desc.origin, + /** + * @param nameOrTestDefinition {string | TestStepDefinition} + * @param fn {(t: TestContext) => void | Promise<void>} + */ + async step(nameOrTestDefinition, fn) { + if (MapPrototypeGet(testStates, desc.id).finalized) { + throw new Error( + "Cannot run test step after parent scope has finished execution. " + + "Ensure any `.step(...)` calls are executed before their parent scope completes execution.", + ); + } + + let stepDesc; + if (typeof nameOrTestDefinition === "string") { + if (!(ObjectPrototypeIsPrototypeOf(FunctionPrototype, fn))) { + throw new TypeError("Expected function for second argument."); + } + stepDesc = { + name: nameOrTestDefinition, + fn, + }; + } else if (typeof nameOrTestDefinition === "object") { + stepDesc = nameOrTestDefinition; + } else { + throw new TypeError( + "Expected a test definition or name and function.", + ); + } + stepDesc.ignore ??= false; + stepDesc.sanitizeOps ??= desc.sanitizeOps; + stepDesc.sanitizeResources ??= desc.sanitizeResources; + stepDesc.sanitizeExit ??= desc.sanitizeExit; + stepDesc.origin = getTestOrigin(); + const jsError = Deno.core.destructureError(new Error()); + stepDesc.location = { + fileName: jsError.frames[1].fileName, + lineNumber: jsError.frames[1].lineNumber, + columnNumber: jsError.frames[1].columnNumber, + }; + stepDesc.level = level + 1; + stepDesc.parent = desc; + stepDesc.rootId = rootId; + stepDesc.rootName = rootName; + const { id } = ops.op_register_test_step(stepDesc); + stepDesc.id = id; + const state = { + context: createTestContext(stepDesc), + children: [], + finalized: false, + status: "pending", + error: null, + elapsed: null, + reportedWait: false, + reportedResult: false, + }; + MapPrototypeSet(testStates, stepDesc.id, state); + ArrayPrototypePush( + MapPrototypeGet(testStates, stepDesc.parent.id).children, + stepDesc, + ); + + try { + if (stepDesc.ignore) { + state.status = "ignored"; + state.finalized = true; + if (canStreamReporting(stepDesc)) { + stepReportResult(stepDesc); + } + return false; + } + + const testFn = wrapTestFnWithSanitizers(stepDesc.fn, stepDesc); + const start = DateNow(); + + try { + await testFn(stepDesc); + + if (failedChildStepsCount(stepDesc) > 0) { + state.status = "failed"; + } else { + state.status = "ok"; + } + } catch (error) { + state.error = error; + state.status = "failed"; + } + + state.elapsed = DateNow() - start; + + if (MapPrototypeGet(testStates, stepDesc.parent.id).finalized) { + // always point this test out as one that was still running + // if the parent step finalized + state.status = "pending"; + } + + state.finalized = true; + + if (state.reportedWait && canStreamReporting(stepDesc)) { + stepReportResult(stepDesc); + } + + return state.status === "ok"; + } finally { + if (canStreamReporting(stepDesc.parent)) { + const parentState = MapPrototypeGet(testStates, stepDesc.parent.id); + // flush any buffered steps + for (const childDesc of parentState.children) { + stepReportResult(childDesc); + } + } + } + }, + }; + } + + /** + * @template T {Function} + * @param testFn {T} + * @param opts {{ + * sanitizeOps: boolean, + * sanitizeResources: boolean, + * sanitizeExit: boolean, + * }} + * @returns {T} + */ + function wrapTestFnWithSanitizers(testFn, opts) { + testFn = assertTestStepScopes(testFn); + + if (opts.sanitizeOps) { + testFn = assertOps(testFn); + } + if (opts.sanitizeResources) { + testFn = assertResources(testFn); + } + if (opts.sanitizeExit) { + testFn = assertExit(testFn, true); + } + return testFn; + } + + window.__bootstrap.internals = { + ...window.__bootstrap.internals ?? {}, + testing: { + runTests, + runBenchmarks, + enableTest, + enableBench, + }, + }; + + window.__bootstrap.denoNs.bench = bench; + window.__bootstrap.denoNs.test = test; +})(this); diff --git a/cli/lsp/testing/execution.rs b/cli/lsp/testing/execution.rs index 950f2a96e..d839cda56 100644 --- a/cli/lsp/testing/execution.rs +++ b/cli/lsp/testing/execution.rs @@ -7,7 +7,6 @@ use super::lsp_custom; use crate::args::flags_from_vec; use crate::args::DenoSubcommand; use crate::checksum; -use crate::create_main_worker; use crate::lsp::client::Client; use crate::lsp::client::TestingNotification; use crate::lsp::config; @@ -16,6 +15,7 @@ use crate::ops; use crate::proc_state; use crate::tools::test; use crate::tools::test::TestEventSender; +use crate::worker::create_main_worker_for_test_or_bench; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; @@ -154,7 +154,7 @@ async fn test_specifier( filter: test::TestFilter, ) -> Result<(), AnyError> { if !token.is_cancelled() { - let mut worker = create_main_worker( + let mut worker = create_main_worker_for_test_or_bench( &ps, specifier.clone(), permissions, diff --git a/cli/main.rs b/cli/main.rs index ed3d459e1..b91540c37 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -18,6 +18,7 @@ mod fs_util; mod graph_util; mod http_cache; mod http_util; +mod js; mod lockfile; mod logger; mod lsp; @@ -289,14 +290,8 @@ async fn eval_command( resolve_url_or_path(&format!("./$deno$eval.{}", eval_flags.ext))?; let permissions = Permissions::from_options(&flags.permissions_options())?; let ps = ProcState::build(flags).await?; - let mut worker = create_main_worker( - &ps, - main_module.clone(), - permissions, - vec![], - Default::default(), - ) - .await?; + let mut worker = + create_main_worker(&ps, main_module.clone(), permissions).await?; // Create a dummy source file. let source_code = if eval_flags.print { format!("console.log({})", eval_flags.code) @@ -602,8 +597,6 @@ async fn repl_command( &ps, main_module.clone(), Permissions::from_options(&ps.options.permissions_options())?, - vec![], - Default::default(), ) .await?; worker.setup_repl().await?; @@ -623,8 +616,6 @@ async fn run_from_stdin(flags: Flags) -> Result<i32, AnyError> { &ps.clone(), main_module.clone(), Permissions::from_options(&ps.options.permissions_options())?, - vec![], - Default::default(), ) .await?; @@ -664,14 +655,8 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<i32, AnyError> { let ps = ProcState::build_for_file_watcher((*flags).clone(), sender.clone()) .await?; - let worker = create_main_worker( - &ps, - main_module.clone(), - permissions, - vec![], - Default::default(), - ) - .await?; + let worker = + create_main_worker(&ps, main_module.clone(), permissions).await?; worker.run_for_watcher().await?; Ok(()) @@ -722,14 +707,8 @@ async fn run_command( }; let permissions = Permissions::from_options(&ps.options.permissions_options())?; - let mut worker = create_main_worker( - &ps, - main_module.clone(), - permissions, - vec![], - Default::default(), - ) - .await?; + let mut worker = + create_main_worker(&ps, main_module.clone(), permissions).await?; let exit_code = worker.run().await?; Ok(exit_code) diff --git a/cli/standalone.rs b/cli/standalone.rs index 5e66bcb18..2742f9bbd 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -287,7 +287,7 @@ pub async fn run( inspect: ps.options.is_inspecting(), }, extensions: ops::cli_exts(ps.clone()), - startup_snapshot: None, + startup_snapshot: Some(crate::js::deno_isolate_init()), unsafely_ignore_certificate_errors: metadata .unsafely_ignore_certificate_errors, root_cert_store: Some(root_cert_store), diff --git a/cli/tests/testdata/test/steps/failing_steps.out b/cli/tests/testdata/test/steps/failing_steps.out index 1e5f2f64d..4df104bd7 100644 --- a/cli/tests/testdata/test/steps/failing_steps.out +++ b/cli/tests/testdata/test/steps/failing_steps.out @@ -37,13 +37,13 @@ failing step in failing test ... FAILED ([WILDCARD]) nested failure => ./test/steps/failing_steps.ts:[WILDCARD] error: Error: 1 test step failed. - at runTest (deno:runtime/js/40_testing.js:[WILDCARD]) - at async runTests (deno:runtime/js/40_testing.js:[WILDCARD]) + at runTest (deno:cli/js/40_testing.js:[WILDCARD]) + at async runTests (deno:cli/js/40_testing.js:[WILDCARD]) multiple test step failures => ./test/steps/failing_steps.ts:[WILDCARD] error: Error: 2 test steps failed. - at runTest (deno:runtime/js/40_testing.js:[WILDCARD]) - at async runTests (deno:runtime/js/40_testing.js:[WILDCARD]) + at runTest (deno:cli/js/40_testing.js:[WILDCARD]) + at async runTests (deno:cli/js/40_testing.js:[WILDCARD]) failing step in failing test => ./test/steps/failing_steps.ts:[WILDCARD] error: Error: Fail test. diff --git a/cli/tools/bench.rs b/cli/tools/bench.rs index c055d8a9c..a81c0a406 100644 --- a/cli/tools/bench.rs +++ b/cli/tools/bench.rs @@ -4,7 +4,6 @@ use crate::args::BenchFlags; use crate::args::Flags; use crate::args::TypeCheckMode; use crate::colors; -use crate::create_main_worker; use crate::file_watcher; use crate::file_watcher::ResolutionResult; use crate::fs_util::collect_specifiers; @@ -15,6 +14,7 @@ use crate::ops; use crate::proc_state::ProcState; use crate::tools::test::format_test_error; use crate::tools::test::TestFilter; +use crate::worker::create_main_worker_for_test_or_bench; use deno_core::error::generic_error; use deno_core::error::AnyError; @@ -352,7 +352,7 @@ async fn bench_specifier( options: BenchSpecifierOptions, ) -> Result<(), AnyError> { let filter = TestFilter::from_flag(&options.filter); - let mut worker = create_main_worker( + let mut worker = create_main_worker_for_test_or_bench( &ps, specifier.clone(), permissions, diff --git a/cli/tools/test.rs b/cli/tools/test.rs index 09257efff..1bb891a1e 100644 --- a/cli/tools/test.rs +++ b/cli/tools/test.rs @@ -5,7 +5,6 @@ use crate::args::TestFlags; use crate::args::TypeCheckMode; use crate::checksum; use crate::colors; -use crate::create_main_worker; use crate::display; use crate::file_fetcher::File; use crate::file_watcher; @@ -18,6 +17,7 @@ use crate::graph_util::contains_specifier; use crate::graph_util::graph_valid; use crate::ops; use crate::proc_state::ProcState; +use crate::worker::create_main_worker_for_test_or_bench; use deno_ast::swc::common::comments::CommentKind; use deno_ast::MediaType; @@ -715,7 +715,7 @@ async fn test_specifier( sender: &TestEventSender, options: TestSpecifierOptions, ) -> Result<(), AnyError> { - let mut worker = create_main_worker( + let mut worker = create_main_worker_for_test_or_bench( &ps, specifier.clone(), permissions, diff --git a/cli/worker.rs b/cli/worker.rs index 7fe1f3c0b..d06864634 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -7,6 +7,9 @@ use deno_core::error::AnyError; use deno_core::futures::task::LocalFutureObj; use deno_core::futures::FutureExt; use deno_core::located_script_name; +use deno_core::serde_json::json; +use deno_core::serde_v8; +use deno_core::v8; use deno_core::Extension; use deno_core::ModuleId; use deno_runtime::colors; @@ -38,6 +41,11 @@ pub struct CliMainWorker { is_main_cjs: bool, worker: MainWorker, ps: ProcState, + + js_run_tests_callback: Option<v8::Global<v8::Function>>, + js_run_benchmarks_callback: Option<v8::Global<v8::Function>>, + js_enable_test_callback: Option<v8::Global<v8::Function>>, + js_enable_bench_callback: Option<v8::Global<v8::Function>>, } impl CliMainWorker { @@ -168,7 +176,7 @@ impl CliMainWorker { &mut self, mode: TestMode, ) -> Result<(), AnyError> { - self.worker.enable_test(); + self.enable_test(); // Enable op call tracing in core to enable better debugging of op sanitizer // failures. @@ -194,10 +202,7 @@ impl CliMainWorker { } self.worker.dispatch_load_event(&located_script_name!())?; - self - .worker - .run_tests(&self.ps.options.shuffle_tests()) - .await?; + self.run_tests(&self.ps.options.shuffle_tests()).await?; loop { if !self .worker @@ -223,7 +228,7 @@ impl CliMainWorker { &mut self, mode: TestMode, ) -> Result<(), AnyError> { - self.worker.enable_test(); + self.enable_test(); self .worker @@ -239,7 +244,7 @@ impl CliMainWorker { } self.worker.dispatch_load_event(&located_script_name!())?; - self.worker.run_tests(&None).await?; + self.run_tests(&None).await?; loop { if !self .worker @@ -254,13 +259,13 @@ impl CliMainWorker { } pub async fn run_bench_specifier(&mut self) -> Result<(), AnyError> { - self.worker.enable_bench(); + self.enable_bench(); // We execute the module module as a side module so that import.meta.main is not set. self.execute_side_module_possibly_with_npm().await?; self.worker.dispatch_load_event(&located_script_name!())?; - self.worker.run_benchmarks().await?; + self.run_benchmarks().await?; loop { if !self .worker @@ -340,14 +345,104 @@ impl CliMainWorker { Ok(None) } } + + /// Run tests declared with `Deno.test()`. Test events will be dispatched + /// by calling ops which are currently only implemented in the CLI crate. + pub async fn run_tests( + &mut self, + shuffle: &Option<u64>, + ) -> Result<(), AnyError> { + let promise = { + let scope = &mut self.worker.js_runtime.handle_scope(); + let cb = self.js_run_tests_callback.as_ref().unwrap().open(scope); + let this = v8::undefined(scope).into(); + let options = + serde_v8::to_v8(scope, json!({ "shuffle": shuffle })).unwrap(); + let promise = cb.call(scope, this, &[options]).unwrap(); + v8::Global::new(scope, promise) + }; + self.worker.js_runtime.resolve_value(promise).await?; + Ok(()) + } + + /// Run benches declared with `Deno.bench()`. Bench events will be dispatched + /// by calling ops which are currently only implemented in the CLI crate. + pub async fn run_benchmarks(&mut self) -> Result<(), AnyError> { + let promise = { + let scope = &mut self.worker.js_runtime.handle_scope(); + let cb = self + .js_run_benchmarks_callback + .as_ref() + .unwrap() + .open(scope); + let this = v8::undefined(scope).into(); + let promise = cb.call(scope, this, &[]).unwrap(); + v8::Global::new(scope, promise) + }; + self.worker.js_runtime.resolve_value(promise).await?; + Ok(()) + } + + /// Enable `Deno.test()`. If this isn't called before executing user code, + /// `Deno.test()` calls will noop. + pub fn enable_test(&mut self) { + let scope = &mut self.worker.js_runtime.handle_scope(); + let cb = self.js_enable_test_callback.as_ref().unwrap().open(scope); + let this = v8::undefined(scope).into(); + cb.call(scope, this, &[]).unwrap(); + } + + /// Enable `Deno.bench()`. If this isn't called before executing user code, + /// `Deno.bench()` calls will noop. + pub fn enable_bench(&mut self) { + let scope = &mut self.worker.js_runtime.handle_scope(); + let cb = self.js_enable_bench_callback.as_ref().unwrap().open(scope); + let this = v8::undefined(scope).into(); + cb.call(scope, this, &[]).unwrap(); + } } pub async fn create_main_worker( ps: &ProcState, main_module: ModuleSpecifier, permissions: Permissions, +) -> Result<CliMainWorker, AnyError> { + create_main_worker_internal( + ps, + main_module, + permissions, + vec![], + Default::default(), + false, + ) + .await +} + +pub async fn create_main_worker_for_test_or_bench( + ps: &ProcState, + main_module: ModuleSpecifier, + permissions: Permissions, + custom_extensions: Vec<Extension>, + stdio: deno_runtime::ops::io::Stdio, +) -> Result<CliMainWorker, AnyError> { + create_main_worker_internal( + ps, + main_module, + permissions, + custom_extensions, + stdio, + true, + ) + .await +} + +async fn create_main_worker_internal( + ps: &ProcState, + main_module: ModuleSpecifier, + permissions: Permissions, mut custom_extensions: Vec<Extension>, stdio: deno_runtime::ops::io::Stdio, + bench_or_test: bool, ) -> Result<CliMainWorker, AnyError> { let (main_module, is_main_cjs) = if let Ok(package_ref) = NpmPackageReference::from_specifier(&main_module) @@ -426,7 +521,7 @@ pub async fn create_main_worker( inspect: ps.options.is_inspecting(), }, extensions, - startup_snapshot: None, + startup_snapshot: Some(crate::js::deno_isolate_init()), unsafely_ignore_certificate_errors: ps .options .unsafely_ignore_certificate_errors() @@ -452,16 +547,59 @@ pub async fn create_main_worker( stdio, }; - let worker = MainWorker::bootstrap_from_options( + let mut worker = MainWorker::bootstrap_from_options( main_module.clone(), permissions, options, ); + + let ( + js_run_tests_callback, + js_run_benchmarks_callback, + js_enable_test_callback, + js_enable_bench_callback, + ) = if bench_or_test { + let scope = &mut worker.js_runtime.handle_scope(); + let js_run_tests_callback = deno_core::JsRuntime::eval::<v8::Function>( + scope, + "Deno[Deno.internal].testing.runTests", + ) + .unwrap(); + let js_run_benchmarks_callback = + deno_core::JsRuntime::eval::<v8::Function>( + scope, + "Deno[Deno.internal].testing.runBenchmarks", + ) + .unwrap(); + let js_enable_tests_callback = deno_core::JsRuntime::eval::<v8::Function>( + scope, + "Deno[Deno.internal].testing.enableTest", + ) + .unwrap(); + let js_enable_bench_callback = deno_core::JsRuntime::eval::<v8::Function>( + scope, + "Deno[Deno.internal].testing.enableBench", + ) + .unwrap(); + ( + Some(v8::Global::new(scope, js_run_tests_callback)), + Some(v8::Global::new(scope, js_run_benchmarks_callback)), + Some(v8::Global::new(scope, js_enable_tests_callback)), + Some(v8::Global::new(scope, js_enable_bench_callback)), + ) + } else { + (None, None, None, None) + }; + Ok(CliMainWorker { main_module, is_main_cjs, worker, ps: ps.clone(), + js_run_tests_callback, + js_run_benchmarks_callback, + js_enable_test_callback, + js_enable_bench_callback, }) } @@ -544,6 +682,7 @@ fn create_web_worker_callback( inspect: ps.options.is_inspecting(), }, extensions, + startup_snapshot: Some(crate::js::deno_isolate_init()), unsafely_ignore_certificate_errors: ps .options .unsafely_ignore_certificate_errors() @@ -577,3 +716,109 @@ fn create_web_worker_callback( ) }) } + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::{resolve_url_or_path, FsModuleLoader}; + use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; + use deno_runtime::deno_web::BlobStore; + + fn create_test_worker() -> MainWorker { + let main_module = resolve_url_or_path("./hello.js").unwrap(); + let permissions = Permissions::default(); + + let options = WorkerOptions { + bootstrap: BootstrapOptions { + args: vec![], + cpu_count: 1, + debug_flag: false, + enable_testing_features: false, + locale: deno_core::v8::icu::get_language_tag(), + location: None, + no_color: true, + is_tty: false, + runtime_version: "x".to_string(), + ts_version: "x".to_string(), + unstable: false, + user_agent: "x".to_string(), + inspect: false, + }, + extensions: vec![], + startup_snapshot: Some(crate::js::deno_isolate_init()), + unsafely_ignore_certificate_errors: None, + root_cert_store: None, + seed: None, + format_js_error_fn: None, + source_map_getter: None, + web_worker_preload_module_cb: Arc::new(|_| unreachable!()), + web_worker_pre_execute_module_cb: Arc::new(|_| unreachable!()), + create_web_worker_cb: Arc::new(|_| unreachable!()), + maybe_inspector_server: None, + should_break_on_first_statement: false, + module_loader: Rc::new(FsModuleLoader), + npm_resolver: None, + get_error_class_fn: None, + cache_storage_dir: None, + origin_storage_dir: None, + blob_store: BlobStore::default(), + broadcast_channel: InMemoryBroadcastChannel::default(), + shared_array_buffer_store: None, + compiled_wasm_module_store: None, + stdio: Default::default(), + }; + + MainWorker::bootstrap_from_options(main_module, permissions, options) + } + + #[tokio::test] + async fn execute_mod_esm_imports_a() { + let p = test_util::testdata_path().join("runtime/esm_imports_a.js"); + let module_specifier = resolve_url_or_path(&p.to_string_lossy()).unwrap(); + let mut worker = create_test_worker(); + let result = worker.execute_main_module(&module_specifier).await; + if let Err(err) = result { + eprintln!("execute_mod err {:?}", err); + } + if let Err(e) = worker.run_event_loop(false).await { + panic!("Future got unexpected error: {:?}", e); + } + } + + #[tokio::test] + async fn execute_mod_circular() { + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("tests/circular1.js"); + let module_specifier = resolve_url_or_path(&p.to_string_lossy()).unwrap(); + let mut worker = create_test_worker(); + let result = worker.execute_main_module(&module_specifier).await; + if let Err(err) = result { + eprintln!("execute_mod err {:?}", err); + } + if let Err(e) = worker.run_event_loop(false).await { + panic!("Future got unexpected error: {:?}", e); + } + } + + #[tokio::test] + async fn execute_mod_resolve_error() { + // "foo" is not a valid module specifier so this should return an error. + let mut worker = create_test_worker(); + let module_specifier = resolve_url_or_path("does-not-exist").unwrap(); + let result = worker.execute_main_module(&module_specifier).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn execute_mod_002_hello() { + // This assumes cwd is project root (an assumption made throughout the + // tests). + let mut worker = create_test_worker(); + let p = test_util::testdata_path().join("run/001_hello.js"); + let module_specifier = resolve_url_or_path(&p.to_string_lossy()).unwrap(); + let result = worker.execute_main_module(&module_specifier).await; + assert!(result.is_ok()); + } +} |