summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2022-03-30 09:59:27 +1100
committerGitHub <noreply@github.com>2022-03-30 09:59:27 +1100
commit061090de7e95e8e7a97f3277bd1a72899ebd1570 (patch)
tree85fbf3ed3dc4cf51a15c2baaf8257a47149c43ef
parent4a0b2c28a15d76c0c40bf07c3753dfbcce4dace1 (diff)
feat(lsp): add experimental testing API (#13798)
Ref: denoland/vscode_deno#629
-rw-r--r--cli/bench/lsp.rs8
-rw-r--r--cli/bench/lsp_bench_standalone.rs2
-rw-r--r--cli/flags.rs6
-rw-r--r--cli/lsp/capabilities.rs1
-rw-r--r--cli/lsp/client.rs40
-rw-r--r--cli/lsp/config.rs39
-rw-r--r--cli/lsp/language_server.rs61
-rw-r--r--cli/lsp/mod.rs1
-rw-r--r--cli/lsp/repl.rs5
-rw-r--r--cli/lsp/testing/collectors.rs619
-rw-r--r--cli/lsp/testing/definitions.rs180
-rw-r--r--cli/lsp/testing/execution.rs947
-rw-r--r--cli/lsp/testing/lsp_custom.rs186
-rw-r--r--cli/lsp/testing/mod.rs11
-rw-r--r--cli/lsp/testing/server.rs219
-rw-r--r--cli/tests/integration/lsp_tests.rs261
-rw-r--r--cli/tests/testdata/lsp/initialize_params.json11
-rw-r--r--cli/tools/test.rs8
-rw-r--r--runtime/js/40_testing.js40
-rw-r--r--test_util/src/lsp.rs13
20 files changed, 2607 insertions, 51 deletions
diff --git a/cli/bench/lsp.rs b/cli/bench/lsp.rs
index a7f712d0b..fdddb9734 100644
--- a/cli/bench/lsp.rs
+++ b/cli/bench/lsp.rs
@@ -44,7 +44,7 @@ struct FixtureMessage {
/// the end of the document and does a level of hovering and gets quick fix
/// code actions.
fn bench_big_file_edits(deno_exe: &Path) -> Result<Duration, AnyError> {
- let mut client = LspClient::new(deno_exe)?;
+ let mut client = LspClient::new(deno_exe, false)?;
let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
let (_, response_error): (Option<Value>, Option<LspResponseError>) =
@@ -125,7 +125,7 @@ fn bench_big_file_edits(deno_exe: &Path) -> Result<Duration, AnyError> {
}
fn bench_code_lens(deno_exe: &Path) -> Result<Duration, AnyError> {
- let mut client = LspClient::new(deno_exe)?;
+ let mut client = LspClient::new(deno_exe, false)?;
let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
let (_, maybe_err) =
@@ -189,7 +189,7 @@ fn bench_code_lens(deno_exe: &Path) -> Result<Duration, AnyError> {
}
fn bench_find_replace(deno_exe: &Path) -> Result<Duration, AnyError> {
- let mut client = LspClient::new(deno_exe)?;
+ let mut client = LspClient::new(deno_exe, false)?;
let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
let (_, maybe_err) =
@@ -285,7 +285,7 @@ fn bench_find_replace(deno_exe: &Path) -> Result<Duration, AnyError> {
/// A test that starts up the LSP, opens a single line document, and exits.
fn bench_startup_shutdown(deno_exe: &Path) -> Result<Duration, AnyError> {
- let mut client = LspClient::new(deno_exe)?;
+ let mut client = LspClient::new(deno_exe, false)?;
let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
let (_, response_error) =
diff --git a/cli/bench/lsp_bench_standalone.rs b/cli/bench/lsp_bench_standalone.rs
index 0caa8620d..b8682f7cd 100644
--- a/cli/bench/lsp_bench_standalone.rs
+++ b/cli/bench/lsp_bench_standalone.rs
@@ -12,7 +12,7 @@ use test_util::lsp::LspClient;
// https://github.com/quick-lint/quick-lint-js/blob/35207e6616267c6c81be63f47ce97ec2452d60df/benchmark/benchmark-lsp/lsp-benchmarks.cpp#L223-L268
fn incremental_change_wait(bench: &mut Bencher) {
let deno_exe = test_util::deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
static FIXTURE_INIT_JSON: &[u8] =
include_bytes!("testdata/initialize_params.json");
diff --git a/cli/flags.rs b/cli/flags.rs
index bf3ea703d..1dda1ef61 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -472,7 +472,11 @@ To evaluate code in the shell:
";
/// Main entry point for parsing deno's command line flags.
-pub fn flags_from_vec(args: Vec<String>) -> clap::Result<Flags> {
+pub fn flags_from_vec<I, T>(args: I) -> clap::Result<Flags>
+where
+ I: IntoIterator<Item = T>,
+ T: Into<std::ffi::OsString> + Clone,
+{
let version = crate::version::deno();
let app = clap_root(&version);
let matches = app.clone().try_get_matches_from(args)?;
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index ed371bab1..9f545d893 100644
--- a/cli/lsp/capabilities.rs
+++ b/cli/lsp/capabilities.rs
@@ -138,6 +138,7 @@ pub fn server_capabilities(
moniker_provider: None,
experimental: Some(json!({
"denoConfigTasks": true,
+ "testingApi":true,
})),
}
}
diff --git a/cli/lsp/client.rs b/cli/lsp/client.rs
index 92a5dfffb..b5728961f 100644
--- a/cli/lsp/client.rs
+++ b/cli/lsp/client.rs
@@ -16,6 +16,14 @@ use crate::lsp::repl::get_repl_workspace_settings;
use super::config::SpecifierSettings;
use super::config::SETTINGS_SECTION;
use super::lsp_custom;
+use super::testing::lsp_custom as testing_lsp_custom;
+
+#[derive(Debug)]
+pub enum TestingNotification {
+ Module(testing_lsp_custom::TestModuleNotificationParams),
+ DeleteModule(testing_lsp_custom::TestModuleDeleteNotificationParams),
+ Progress(testing_lsp_custom::TestRunProgressParams),
+}
#[derive(Clone)]
pub struct Client(Arc<dyn ClientTrait>);
@@ -51,6 +59,10 @@ impl Client {
self.0.send_registry_state_notification(params).await;
}
+ pub fn send_test_notification(&self, params: TestingNotification) {
+ self.0.send_test_notification(params);
+ }
+
pub async fn specifier_configurations(
&self,
specifiers: Vec<lsp::Url>,
@@ -118,6 +130,7 @@ trait ClientTrait: Send + Sync {
&self,
params: lsp_custom::RegistryStateNotificationParams,
) -> AsyncReturn<()>;
+ fn send_test_notification(&self, params: TestingNotification);
fn specifier_configurations(
&self,
uris: Vec<lsp::Url>,
@@ -164,6 +177,31 @@ impl ClientTrait for LspowerClient {
})
}
+ fn send_test_notification(&self, notification: TestingNotification) {
+ let client = self.0.clone();
+ tokio::task::spawn(async move {
+ match notification {
+ TestingNotification::Module(params) => {
+ client
+ .send_custom_notification::<testing_lsp_custom::TestModuleNotification>(
+ params,
+ )
+ .await
+ }
+ TestingNotification::DeleteModule(params) => client
+ .send_custom_notification::<testing_lsp_custom::TestModuleDeleteNotification>(
+ params,
+ )
+ .await,
+ TestingNotification::Progress(params) => client
+ .send_custom_notification::<testing_lsp_custom::TestRunProgressNotification>(
+ params,
+ )
+ .await,
+ }
+ });
+ }
+
fn specifier_configurations(
&self,
uris: Vec<lsp::Url>,
@@ -260,6 +298,8 @@ impl ClientTrait for ReplClient {
Box::pin(future::ready(()))
}
+ fn send_test_notification(&self, _params: TestingNotification) {}
+
fn specifier_configurations(
&self,
uris: Vec<lsp::Url>,
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index 7b4294943..6cd401b0c 100644
--- a/cli/lsp/config.rs
+++ b/cli/lsp/config.rs
@@ -21,6 +21,10 @@ pub struct ClientCapabilities {
pub code_action_disabled_support: bool,
pub line_folding_only: bool,
pub status_notification: bool,
+ /// The client provides the `experimental.testingApi` capability, which is
+ /// built around VSCode's testing API. It indicates that the server should
+ /// send notifications about tests discovered in modules.
+ pub testing_api: bool,
pub workspace_configuration: bool,
pub workspace_did_change_watched_files: bool,
}
@@ -139,6 +143,28 @@ pub struct SpecifierSettings {
pub code_lens: CodeLensSpecifierSettings,
}
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct TestingSettings {
+ /// A vector of arguments which should be used when running the tests for
+ /// a workspace.
+ #[serde(default)]
+ pub args: Vec<String>,
+ /// Enable or disable the testing API if the client is capable of supporting
+ /// the testing API.
+ #[serde(default = "is_true")]
+ pub enable: bool,
+}
+
+impl Default for TestingSettings {
+ fn default() -> Self {
+ Self {
+ args: vec!["--allow-all".to_string(), "--no-check".to_string()],
+ enable: true,
+ }
+ }
+}
+
/// Deno language server specific settings that are applied to a workspace.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
@@ -184,6 +210,10 @@ pub struct WorkspaceSettings {
#[serde(default)]
pub suggest: CompletionSettings,
+ /// Testing settings for the workspace.
+ #[serde(default)]
+ pub testing: TestingSettings,
+
/// An option which sets the cert file to use when attempting to fetch remote
/// resources. This overrides `DENO_CERT` if present.
pub tls_certificate: Option<String>,
@@ -333,7 +363,10 @@ impl Config {
self.client_capabilities.status_notification = experimental
.get("statusNotification")
.and_then(|it| it.as_bool())
- == Some(true)
+ == Some(true);
+ self.client_capabilities.testing_api =
+ experimental.get("testingApi").and_then(|it| it.as_bool())
+ == Some(true);
}
if let Some(workspace) = &capabilities.workspace {
@@ -530,6 +563,10 @@ mod tests {
hosts: HashMap::new(),
}
},
+ testing: TestingSettings {
+ args: vec!["--allow-all".to_string(), "--no-check".to_string()],
+ enable: true
+ },
tls_certificate: None,
unsafely_ignore_certificate_errors: None,
unstable: false,
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index e0d99aa69..4555e3b85 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -47,6 +47,7 @@ use super::performance::Performance;
use super::refactor;
use super::registries::ModuleRegistry;
use super::registries::ModuleRegistryOptions;
+use super::testing;
use super::text;
use super::tsc;
use super::tsc::Assets;
@@ -107,14 +108,16 @@ pub struct Inner {
/// An optional configuration file which has been specified in the client
/// options.
maybe_config_file: Option<ConfigFile>,
- /// An optional configuration for linter which has been taken from specified config file.
- pub maybe_lint_config: Option<LintConfig>,
/// An optional configuration for formatter which has been taken from specified config file.
maybe_fmt_config: Option<FmtConfig>,
/// An optional import map which is used to resolve modules.
pub maybe_import_map: Option<Arc<ImportMap>>,
/// The URL for the import map which is used to determine relative imports.
maybe_import_map_uri: Option<Url>,
+ /// An optional configuration for linter which has been taken from specified config file.
+ pub maybe_lint_config: Option<LintConfig>,
+ /// A lazily create "server" for handling test run requests.
+ maybe_testing_server: Option<testing::TestServer>,
/// A collection of measurements which instrument that performance of the LSP.
performance: Arc<Performance>,
/// A memoized version of fixable diagnostic codes retrieved from TypeScript.
@@ -163,12 +166,13 @@ impl Inner {
diagnostics_server,
documents,
maybe_cache_path: None,
- maybe_lint_config: None,
- maybe_fmt_config: None,
maybe_cache_server: None,
maybe_config_file: None,
maybe_import_map: None,
maybe_import_map_uri: None,
+ maybe_lint_config: None,
+ maybe_fmt_config: None,
+ maybe_testing_server: None,
module_registries,
module_registries_location,
performance,
@@ -781,6 +785,15 @@ impl Inner {
}
self.config.update_enabled_paths(self.client.clone()).await;
+ if self.config.client_capabilities.testing_api {
+ let test_server = testing::TestServer::new(
+ self.client.clone(),
+ self.performance.clone(),
+ self.config.root_uri.clone(),
+ );
+ self.maybe_testing_server = Some(test_server);
+ }
+
lsp_log!("Server ready.");
}
@@ -835,6 +848,7 @@ impl Inner {
.diagnostics_server
.invalidate(&self.documents.dependents(&specifier));
self.send_diagnostics_update();
+ self.send_testing_update();
}
}
Err(err) => error!("{}", err),
@@ -860,6 +874,7 @@ impl Inner {
specifiers.push(specifier.clone());
self.diagnostics_server.invalidate(&specifiers);
self.send_diagnostics_update();
+ self.send_testing_update();
}
self.performance.measure(mark);
}
@@ -909,6 +924,7 @@ impl Inner {
);
self.send_diagnostics_update();
+ self.send_testing_update();
}
async fn did_change_watched_files(
@@ -954,6 +970,7 @@ impl Inner {
);
self.diagnostics_server.invalidate_all();
self.send_diagnostics_update();
+ self.send_testing_update();
}
self.performance.measure(mark);
}
@@ -2143,6 +2160,29 @@ impl Inner {
self.reload_import_registries().await
}
lsp_custom::TASK_REQUEST => self.get_tasks(),
+ testing::TEST_RUN_REQUEST => {
+ if let Some(testing_server) = &self.maybe_testing_server {
+ match params.map(serde_json::from_value) {
+ Some(Ok(params)) => testing_server
+ .run_request(params, self.config.get_workspace_settings()),
+ Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
+ None => Err(LspError::invalid_params("Missing parameters")),
+ }
+ } else {
+ Err(LspError::invalid_request())
+ }
+ }
+ testing::TEST_RUN_CANCEL_REQUEST => {
+ if let Some(testing_server) = &self.maybe_testing_server {
+ match params.map(serde_json::from_value) {
+ Some(Ok(params)) => testing_server.run_cancel_request(params),
+ Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
+ None => Err(LspError::invalid_params("Missing parameters")),
+ }
+ } else {
+ Err(LspError::invalid_request())
+ }
+ }
lsp_custom::VIRTUAL_TEXT_DOCUMENT => {
match params.map(serde_json::from_value) {
Some(Ok(params)) => Ok(Some(
@@ -2389,6 +2429,16 @@ impl Inner {
error!("Cannot update diagnostics: {}", err);
}
}
+
+ /// Send a message to the testing server to look for any changes in tests and
+ /// update the client.
+ fn send_testing_update(&self) {
+ if let Some(testing_server) = &self.maybe_testing_server {
+ if let Err(err) = testing_server.update(self.snapshot()) {
+ error!("Cannot update testing server: {}", err);
+ }
+ }
+ }
}
#[lspower::async_trait]
@@ -2432,6 +2482,7 @@ impl lspower::LanguageServer for LanguageServer {
// don't send diagnostics yet if we don't have the specifier settings
if has_specifier_settings {
inner.send_diagnostics_update();
+ inner.send_testing_update();
}
}
(client, uri, specifier, has_specifier_settings)
@@ -2464,6 +2515,7 @@ impl lspower::LanguageServer for LanguageServer {
.unwrap_or(false)
{
inner.send_diagnostics_update();
+ inner.send_testing_update();
}
});
}
@@ -2823,6 +2875,7 @@ impl Inner {
// invalidate some diagnostics
self.diagnostics_server.invalidate(&[referrer]);
self.send_diagnostics_update();
+ self.send_testing_update();
self.performance.measure(mark);
Ok(Some(json!(true)))
diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs
index afaf47548..1cfd3041c 100644
--- a/cli/lsp/mod.rs
+++ b/cli/lsp/mod.rs
@@ -26,6 +26,7 @@ mod refactor;
mod registries;
mod repl;
mod semantic_tokens;
+mod testing;
mod text;
mod tsc;
mod urls;
diff --git a/cli/lsp/repl.rs b/cli/lsp/repl.rs
index 1026c7bda..44634580d 100644
--- a/cli/lsp/repl.rs
+++ b/cli/lsp/repl.rs
@@ -36,6 +36,7 @@ use lspower::LanguageServer;
use super::client::Client;
use super::config::CompletionSettings;
use super::config::ImportCompletionSettings;
+use super::config::TestingSettings;
use super::config::WorkspaceSettings;
#[derive(Debug)]
@@ -294,5 +295,9 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings {
hosts: HashMap::from([("https://deno.land".to_string(), true)]),
},
},
+ testing: TestingSettings {
+ args: vec![],
+ enable: false,
+ },
}
}
diff --git a/cli/lsp/testing/collectors.rs b/cli/lsp/testing/collectors.rs
new file mode 100644
index 000000000..572c03726
--- /dev/null
+++ b/cli/lsp/testing/collectors.rs
@@ -0,0 +1,619 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use super::definitions::TestDefinition;
+
+use deno_ast::swc::ast;
+use deno_ast::swc::common::Span;
+use deno_ast::swc::visit::Visit;
+use deno_ast::swc::visit::VisitWith;
+use deno_core::ModuleSpecifier;
+use std::collections::HashSet;
+
+/// Parse an arrow expression for any test steps and return them.
+fn arrow_to_steps(
+ parent: &str,
+ level: usize,
+ arrow_expr: &ast::ArrowExpr,
+) -> Option<Vec<TestDefinition>> {
+ if let Some((maybe_test_context, maybe_step_var)) =
+ parse_test_context_param(arrow_expr.params.get(0))
+ {
+ let mut collector = TestStepCollector::new(
+ parent.to_string(),
+ level,
+ maybe_test_context,
+ maybe_step_var,
+ );
+ arrow_expr.body.visit_with(&mut collector);
+ let steps = collector.take();
+ if !steps.is_empty() {
+ Some(steps)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+}
+
+/// Parse a function for any test steps and return them.
+fn fn_to_steps(
+ parent: &str,
+ level: usize,
+ function: &ast::Function,
+) -> Option<Vec<TestDefinition>> {
+ if let Some((maybe_test_context, maybe_step_var)) =
+ parse_test_context_param(function.params.get(0).map(|p| &p.pat))
+ {
+ let mut collector = TestStepCollector::new(
+ parent.to_string(),
+ level,
+ maybe_test_context,
+ maybe_step_var,
+ );
+ function.body.visit_with(&mut collector);
+ let steps = collector.take();
+ if !steps.is_empty() {
+ Some(steps)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+}
+
+/// Parse a param of a test function for the test context binding, or any
+/// destructuring of a `steps` method from the test context.
+fn parse_test_context_param(
+ param: Option<&ast::Pat>,
+) -> Option<(Option<String>, Option<String>)> {
+ let mut maybe_test_context = None;
+ let mut maybe_step_var = None;
+ match param {
+ // handles `(testContext)`
+ Some(ast::Pat::Ident(binding_ident)) => {
+ maybe_test_context = Some(binding_ident.id.sym.to_string());
+ }
+ Some(ast::Pat::Object(object_pattern)) => {
+ for prop in &object_pattern.props {
+ match prop {
+ ast::ObjectPatProp::KeyValue(key_value_pat_prop) => {
+ match &key_value_pat_prop.key {
+ // handles `({ step: s })`
+ ast::PropName::Ident(ident) => {
+ if ident.sym.eq("step") {
+ if let ast::Pat::Ident(ident) =
+ key_value_pat_prop.value.as_ref()
+ {
+ maybe_step_var = Some(ident.id.sym.to_string());
+ }
+ break;
+ }
+ }
+ // handles `({ "step": s })`
+ ast::PropName::Str(string) => {
+ if string.value.eq("step") {
+ if let ast::Pat::Ident(ident) =
+ key_value_pat_prop.value.as_ref()
+ {
+ maybe_step_var = Some(ident.id.sym.to_string());
+ }
+ break;
+ }
+ }
+ _ => (),
+ }
+ }
+ // handles `({ step = something })`
+ ast::ObjectPatProp::Assign(assign_pat_prop)
+ if assign_pat_prop.key.sym.eq("step") =>
+ {
+ maybe_step_var = Some("step".to_string());
+ break;
+ }
+ // handles `({ ...ctx })`
+ ast::ObjectPatProp::Rest(rest_pat) => {
+ if let ast::Pat::Ident(ident) = rest_pat.arg.as_ref() {
+ maybe_test_context = Some(ident.id.sym.to_string());
+ }
+ break;
+ }
+ _ => (),
+ }
+ }
+ }
+ _ => return None,
+ }
+ if maybe_test_context.is_none() && maybe_step_var.is_none() {
+ None
+ } else {
+ Some((maybe_test_context, maybe_step_var))
+ }
+}
+
+/// Check a call expression of a test or test step to determine the name of the
+/// test or test step as well as any sub steps.
+fn check_call_expr(
+ parent: &str,
+ node: &ast::CallExpr,
+ level: usize,
+) -> Option<(String, Option<Vec<TestDefinition>>)> {
+ if let Some(expr) = node.args.get(0).map(|es| es.expr.as_ref()) {
+ match expr {
+ ast::Expr::Object(obj_lit) => {
+ let mut maybe_name = None;
+ let mut steps = None;
+ for prop in &obj_lit.props {
+ if let ast::PropOrSpread::Prop(prop) = prop {
+ match prop.as_ref() {
+ ast::Prop::KeyValue(key_value_prop) => {
+ if let ast::PropName::Ident(ast::Ident { sym, .. }) =
+ &key_value_prop.key
+ {
+ match sym.to_string().as_str() {
+ "name" => match key_value_prop.value.as_ref() {
+ // matches string literals (e.g. "test name" or
+ // 'test name')
+ ast::Expr::Lit(ast::Lit::Str(lit_str)) => {
+ maybe_name = Some(lit_str.value.to_string());
+ }
+ // matches template literals with only a single quasis
+ // (e.g. `test name`)
+ ast::Expr::Tpl(tpl) => {
+ if tpl.quasis.len() == 1 {
+ if let Some(tpl_element) = tpl.quasis.get(0) {
+ maybe_name =
+ Some(tpl_element.raw.value.to_string());
+ }
+ }
+ }
+ _ => (),
+ },
+ "fn" => match key_value_prop.value.as_ref() {
+ ast::Expr::Arrow(arrow_expr) => {
+ steps = arrow_to_steps(parent, level, arrow_expr);
+ }
+ ast::Expr::Fn(fn_expr) => {
+ steps = fn_to_steps(parent, level, &fn_expr.function);
+ }
+ _ => (),
+ },
+ _ => (),
+ }
+ }
+ }
+ ast::Prop::Method(method_prop) => {
+ steps = fn_to_steps(parent, level, &method_prop.function);
+ }
+ _ => (),
+ }
+ }
+ }
+ maybe_name.map(|name| (name, steps))
+ }
+ ast::Expr::Fn(fn_expr) => {
+ if let Some(ast::Ident { sym, .. }) = fn_expr.ident.as_ref() {
+ let name = sym.to_string();
+ let steps = fn_to_steps(parent, level, &fn_expr.function);
+ Some((name, steps))
+ } else {
+ None
+ }
+ }
+ ast::Expr::Lit(ast::Lit::Str(lit_str)) => {
+ let name = lit_str.value.to_string();
+ let mut steps = None;
+ match node.args.get(1).map(|es| es.expr.as_ref()) {
+ Some(ast::Expr::Fn(fn_expr)) => {
+ steps = fn_to_steps(parent, level, &fn_expr.function);
+ }
+ Some(ast::Expr::Arrow(arrow_expr)) => {
+ steps = arrow_to_steps(parent, level, arrow_expr);
+ }
+ _ => (),
+ }
+ Some((name, steps))
+ }
+ _ => None,
+ }
+ } else {
+ None
+ }
+}
+
+/// A structure which can be used to walk a branch of AST determining if the
+/// branch contains any testing steps.
+struct TestStepCollector {
+ steps: Vec<TestDefinition>,
+ level: usize,
+ parent: String,
+ maybe_test_context: Option<String>,
+ vars: HashSet<String>,
+}
+
+impl TestStepCollector {
+ fn new(
+ parent: String,
+ level: usize,
+ maybe_test_context: Option<String>,
+ maybe_step_var: Option<String>,
+ ) -> Self {
+ let mut vars = HashSet::new();
+ if let Some(var) = maybe_step_var {
+ vars.insert(var);
+ }
+ Self {
+ steps: Vec::default(),
+ level,
+ parent,
+ maybe_test_context,
+ vars,
+ }
+ }
+
+ fn add_step<N: AsRef<str>>(
+ &mut self,
+ name: N,
+ span: &Span,
+ steps: Option<Vec<TestDefinition>>,
+ ) {
+ let step = TestDefinition::new_step(
+ name.as_ref().to_string(),
+ *span,
+ self.parent.clone(),
+ self.level,
+ steps,
+ );
+ self.steps.push(step);
+ }
+
+ fn check_call_expr(&mut self, node: &ast::CallExpr, span: &Span) {
+ if let Some((name, steps)) =
+ check_call_expr(&self.parent, node, self.level + 1)
+ {
+ self.add_step(name, span, steps);
+ }
+ }
+
+ /// Move out the test definitions
+ pub fn take(self) -> Vec<TestDefinition> {
+ self.steps
+ }
+}
+
+impl Visit for TestStepCollector {
+ fn visit_call_expr(&mut self, node: &ast::CallExpr) {
+ if let ast::Callee::Expr(callee_expr) = &node.callee {
+ match callee_expr.as_ref() {
+ // Identify calls to identified variables
+ ast::Expr::Ident(ident) => {
+ if self.vars.contains(&ident.sym.to_string()) {
+ self.check_call_expr(node, &ident.span);
+ }
+ }
+ // Identify calls to `test.step()`
+ ast::Expr::Member(member_expr) => {
+ if let Some(test_context) = &self.maybe_test_context {
+ if let ast::MemberProp::Ident(ns_prop_ident) = &member_expr.prop {
+ if ns_prop_ident.sym.eq("step") {
+ if let ast::Expr::Ident(ident) = member_expr.obj.as_ref() {
+ if ident.sym == *test_context {
+ self.check_call_expr(node, &ns_prop_ident.span);
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+
+ fn visit_var_decl(&mut self, node: &ast::VarDecl) {
+ if let Some(test_context) = &self.maybe_test_context {
+ for decl in &node.decls {
+ if let Some(init) = &decl.init {
+ match init.as_ref() {
+ // Identify destructured assignments of `step` from test context
+ ast::Expr::Ident(ident) => {
+ if ident.sym == *test_context {
+ if let ast::Pat::Object(object_pat) = &decl.name {
+ for prop in &object_pat.props {
+ match prop {
+ ast::ObjectPatProp::Assign(prop) => {
+ if prop.key.sym.eq("step") {
+ self.vars.insert(prop.key.sym.to_string());
+ }
+ }
+ ast::ObjectPatProp::KeyValue(prop) => {
+ if let ast::PropName::Ident(key_ident) = &prop.key {
+ if key_ident.sym.eq("step") {
+ if let ast::Pat::Ident(value_ident) =
+ &prop.value.as_ref()
+ {
+ self.vars.insert(value_ident.id.sym.to_string());
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+ // Identify variable assignments where the init is test context
+ // `.step`
+ ast::Expr::Member(member_expr) => {
+ if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() {
+ if obj_ident.sym == *test_context {
+ if let ast::MemberProp::Ident(prop_ident) = &member_expr.prop
+ {
+ if prop_ident.sym.eq("step") {
+ if let ast::Pat::Ident(binding_ident) = &decl.name {
+ self.vars.insert(binding_ident.id.sym.to_string());
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+}
+
+/// Walk an AST and determine if it contains any `Deno.test` tests.
+pub struct TestCollector {
+ definitions: Vec<TestDefinition>,
+ specifier: ModuleSpecifier,
+ vars: HashSet<String>,
+}
+
+impl TestCollector {
+ pub fn new(specifier: ModuleSpecifier) -> Self {
+ Self {
+ definitions: Vec::new(),
+ specifier,
+ vars: HashSet::new(),
+ }
+ }
+
+ fn add_definition<N: AsRef<str>>(
+ &mut self,
+ name: N,
+ span: &Span,
+ steps: Option<Vec<TestDefinition>>,
+ ) {
+ let definition = TestDefinition::new(
+ &self.specifier,
+ name.as_ref().to_string(),
+ *span,
+ steps,
+ );
+ self.definitions.push(definition);
+ }
+
+ fn check_call_expr(&mut self, node: &ast::CallExpr, span: &Span) {
+ if let Some((name, steps)) =
+ check_call_expr(self.specifier.as_str(), node, 1)
+ {
+ self.add_definition(name, span, steps);
+ }
+ }
+
+ /// Move out the test definitions
+ pub fn take(self) -> Vec<TestDefinition> {
+ self.definitions
+ }
+}
+
+impl Visit for TestCollector {
+ fn visit_call_expr(&mut self, node: &ast::CallExpr) {
+ if let ast::Callee::Expr(callee_expr) = &node.callee {
+ match callee_expr.as_ref() {
+ ast::Expr::Ident(ident) => {
+ if self.vars.contains(&ident.sym.to_string()) {
+ self.check_call_expr(node, &ident.span);
+ }
+ }
+ ast::Expr::Member(member_expr) => {
+ if let ast::MemberProp::Ident(ns_prop_ident) = &member_expr.prop {
+ if ns_prop_ident.sym.to_string() == "test" {
+ if let ast::Expr::Ident(ident) = member_expr.obj.as_ref() {
+ if ident.sym.to_string() == "Deno" {
+ self.check_call_expr(node, &ns_prop_ident.span);
+ }
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+
+ fn visit_var_decl(&mut self, node: &ast::VarDecl) {
+ for decl in &node.decls {
+ if let Some(init) = &decl.init {
+ match init.as_ref() {
+ // Identify destructured assignments of `test` from `Deno`
+ ast::Expr::Ident(ident) => {
+ if ident.sym.to_string() == "Deno" {
+ if let ast::Pat::Object(object_pat) = &decl.name {
+ for prop in &object_pat.props {
+ match prop {
+ ast::ObjectPatProp::Assign(prop) => {
+ let name = prop.key.sym.to_string();
+ if name == "test" {
+ self.vars.insert(name);
+ }
+ }
+ ast::ObjectPatProp::KeyValue(prop) => {
+ if let ast::PropName::Ident(key_ident) = &prop.key {
+ if key_ident.sym.to_string() == "test" {
+ if let ast::Pat::Ident(value_ident) =
+ &prop.value.as_ref()
+ {
+ self.vars.insert(value_ident.id.sym.to_string());
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+ // Identify variable assignments where the init is `Deno.test`
+ ast::Expr::Member(member_expr) => {
+ if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() {
+ if obj_ident.sym.to_string() == "Deno" {
+ if let ast::MemberProp::Ident(prop_ident) = &member_expr.prop {
+ if prop_ident.sym.to_string() == "test" {
+ if let ast::Pat::Ident(binding_ident) = &decl.name {
+ self.vars.insert(binding_ident.id.sym.to_string());
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use deno_ast::swc::common::BytePos;
+ use deno_ast::swc::common::SyntaxContext;
+ use deno_core::resolve_url;
+ use std::sync::Arc;
+
+ pub fn new_span(lo: u32, hi: u32, ctxt: u32) -> Span {
+ Span {
+ lo: BytePos(lo),
+ hi: BytePos(hi),
+ ctxt: SyntaxContext::from_u32(ctxt),
+ }
+ }
+
+ #[test]
+ fn test_test_collector() {
+ let specifier = resolve_url("file:///a/example.ts").unwrap();
+ let source = Arc::new(
+ r#"
+ Deno.test({
+ name: "test a",
+ async fn(t) {
+ await t.step("a step", ({ step }) => {
+ await step({
+ name: "sub step",
+ fn() {}
+ })
+ });
+ }
+ });
+
+ Deno.test(async function useFnName({ step: s }) {
+ await s("step c", () => {});
+ });
+
+ Deno.test("test b", () => {});
+
+ const { test } = Deno;
+ test("test c", () => {});
+
+ const t = Deno.test;
+ t("test d", () => {});
+ "#
+ .to_string(),
+ );
+
+ let parsed_module = deno_ast::parse_module(deno_ast::ParseParams {
+ specifier: specifier.to_string(),
+ source: deno_ast::SourceTextInfo::new(source),
+ media_type: deno_ast::MediaType::TypeScript,
+ capture_tokens: true,
+ scope_analysis: true,
+ maybe_syntax: None,
+ })
+ .unwrap();
+ let mut collector = TestCollector::new(specifier);
+ parsed_module.module().visit_with(&mut collector);
+ assert_eq!(
+ collector.take(),
+ vec![
+ TestDefinition {
+ id: "cf31850c831233526df427cdfd25b6b84b2af0d6ce5f8ee1d22c465234b46348".to_string(),
+ level: 0,
+ name: "test a".to_string(),
+ span: new_span(12, 16, 0),
+ steps: Some(vec![
+ TestDefinition {
+ id: "4c7333a1e47721631224408c467f32751fe34b876cab5ec1f6ac71980ff15ad3".to_string(),
+ level: 1,
+ name: "a step".to_string(),
+ span: new_span(83, 87, 0),
+ steps: Some(vec![
+ TestDefinition {
+ id: "abf356f59139b77574089615f896a6f501c010985d95b8a93abeb0069ccb2201".to_string(),
+ level: 2,
+ name: "sub step".to_string(),
+ span: new_span(132, 136, 3),
+ steps: None,
+ }
+ ])
+ }
+ ]),
+ },
+ TestDefinition {
+ id: "86b4c821900e38fc89f24bceb0e45193608ab3f9d2a6019c7b6a5aceff5d7df2".to_string(),
+ level: 0,
+ name: "useFnName".to_string(),
+ span: new_span(254, 258, 0),
+ steps: Some(vec![
+ TestDefinition {
+ id: "67a390d0084ae5fb88f3510c470a72a553581f1d0d5ba5fa89aee7a754f3953a".to_string(),
+ level: 1,
+ name: "step c".to_string(),
+ span: new_span(313, 314, 4),
+ steps: None,
+ }
+ ])
+ },
+ TestDefinition {
+ id: "580eda89d7f5e619774c20e13b7d07a8e77c39cba101d60565144d48faa837cb".to_string(),
+ level: 0,
+ name: "test b".to_string(),
+ span: new_span(358, 362, 0),
+ steps: None,
+ },
+ TestDefinition {
+ id: "0b7c6bf3cd617018d33a1bf982a08fe088c5bb54fcd5eb9e802e7c137ec1af94".to_string(),
+ level: 0,
+ name: "test c".to_string(),
+ span: new_span(420, 424, 1),
+ steps: None,
+ },
+ TestDefinition {
+ id: "69d9fe87f64f5b66cb8b631d4fd2064e8224b8715a049be54276c42189ff8f9f".to_string(),
+ level: 0,
+ name: "test d".to_string(),
+ span: new_span(480, 481, 1),
+ steps: None,
+ }
+ ]
+ );
+ }
+}
diff --git a/cli/lsp/testing/definitions.rs b/cli/lsp/testing/definitions.rs
new file mode 100644
index 000000000..0fa6a8fd5
--- /dev/null
+++ b/cli/lsp/testing/definitions.rs
@@ -0,0 +1,180 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use super::lsp_custom;
+
+use crate::checksum;
+use crate::lsp::client::TestingNotification;
+
+use deno_ast::swc::common::Span;
+use deno_ast::SourceTextInfo;
+use deno_core::ModuleSpecifier;
+use lspower::lsp;
+use std::collections::HashMap;
+
+fn span_to_range(
+ span: &Span,
+ source_text_info: &SourceTextInfo,
+) -> Option<lsp::Range> {
+ let start = source_text_info.line_and_column_index(span.lo);
+ let end = source_text_info.line_and_column_index(span.hi);
+ Some(lsp::Range {
+ start: lsp::Position {
+ line: start.line_index as u32,
+ character: start.column_index as u32,
+ },
+ end: lsp::Position {
+ line: end.line_index as u32,
+ character: end.column_index as u32,
+ },
+ })
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct TestDefinition {
+ pub id: String,
+ pub level: usize,
+ pub name: String,
+ pub span: Span,
+ pub steps: Option<Vec<TestDefinition>>,
+}
+
+impl TestDefinition {
+ pub fn new(
+ specifier: &ModuleSpecifier,
+ name: String,
+ span: Span,
+ steps: Option<Vec<TestDefinition>>,
+ ) -> Self {
+ let id = checksum::gen(&[specifier.as_str().as_bytes(), name.as_bytes()]);
+ Self {
+ id,
+ level: 0,
+ name,
+ span,
+ steps,
+ }
+ }
+
+ pub fn new_step(
+ name: String,
+ span: Span,
+ parent: String,
+ level: usize,
+ steps: Option<Vec<TestDefinition>>,
+ ) -> Self {
+ let id = checksum::gen(&[
+ parent.as_bytes(),
+ &level.to_be_bytes(),
+ name.as_bytes(),
+ ]);
+ Self {
+ id,
+ level,
+ name,
+ span,
+ steps,
+ }
+ }
+
+ fn as_test_data(
+ &self,
+ source_text_info: &SourceTextInfo,
+ ) -> lsp_custom::TestData {
+ lsp_custom::TestData {
+ id: self.id.clone(),
+ label: self.name.clone(),
+ steps: self.steps.as_ref().map(|steps| {
+ steps
+ .iter()
+ .map(|step| step.as_test_data(source_text_info))
+ .collect()
+ }),
+ range: span_to_range(&self.span, source_text_info),
+ }
+ }
+
+ fn find_step(&self, name: &str, level: usize) -> Option<&TestDefinition> {
+ if let Some(steps) = &self.steps {
+ for step in steps {
+ if step.name == name && step.level == level {
+ return Some(step);
+ } else if let Some(step) = step.find_step(name, level) {
+ return Some(step);
+ }
+ }
+ }
+ None
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct TestDefinitions {
+ /// definitions of tests and their steps which were statically discovered from
+ /// the source document.
+ pub discovered: Vec<TestDefinition>,
+ /// Tests and steps which the test runner notified us of, which were
+ /// dynamically added
+ pub injected: Vec<lsp_custom::TestData>,
+ /// The version of the document that the discovered tests relate to.
+ pub script_version: String,
+}
+
+impl TestDefinitions {
+ /// Return the test definitions as a testing module notification.
+ pub fn as_notification(
+ &self,
+ specifier: &ModuleSpecifier,
+ maybe_root: Option<&ModuleSpecifier>,
+ source_text_info: &SourceTextInfo,
+ ) -> TestingNotification {
+ let label = if let Some(root) = maybe_root {
+ specifier.as_str().replace(root.as_str(), "")
+ } else {
+ specifier
+ .path_segments()
+ .and_then(|s| s.last().map(|s| s.to_string()))
+ .unwrap_or_else(|| "<unknown>".to_string())
+ };
+ let mut tests_map: HashMap<String, lsp_custom::TestData> = self
+ .injected
+ .iter()
+ .map(|td| (td.id.clone(), td.clone()))
+ .collect();
+ tests_map.extend(self.discovered.iter().map(|td| {
+ let test_data = td.as_test_data(source_text_info);
+ (test_data.id.clone(), test_data)
+ }));
+ TestingNotification::Module(lsp_custom::TestModuleNotificationParams {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: specifier.clone(),
+ },
+ kind: lsp_custom::TestModuleNotificationKind::Replace,
+ label,
+ tests: tests_map.into_values().collect(),
+ })
+ }
+
+ /// Return a test definition identified by the test ID.
+ pub fn get_by_id<S: AsRef<str>>(&self, id: S) -> Option<&TestDefinition> {
+ self
+ .discovered
+ .iter()
+ .find(|td| td.id.as_str() == id.as_ref())
+ }
+
+ /// Return a test definition by the test name.
+ pub fn get_by_name(&self, name: &str) -> Option<&TestDefinition> {
+ self.discovered.iter().find(|td| td.name.as_str() == name)
+ }
+
+ pub fn get_step_by_name(
+ &self,
+ test_name: &str,
+ level: usize,
+ name: &str,
+ ) -> Option<&TestDefinition> {
+ self
+ .get_by_name(test_name)
+ .and_then(|td| td.find_step(name, level))
+ }
+}
diff --git a/cli/lsp/testing/execution.rs b/cli/lsp/testing/execution.rs
new file mode 100644
index 000000000..03436ad6a
--- /dev/null
+++ b/cli/lsp/testing/execution.rs
@@ -0,0 +1,947 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use super::definitions::TestDefinition;
+use super::definitions::TestDefinitions;
+use super::lsp_custom;
+
+use crate::checksum;
+use crate::create_main_worker;
+use crate::emit;
+use crate::flags;
+use crate::located_script_name;
+use crate::lsp::client::Client;
+use crate::lsp::client::TestingNotification;
+use crate::lsp::config;
+use crate::lsp::logging::lsp_log;
+use crate::ops;
+use crate::proc_state;
+use crate::tools::test;
+
+use deno_core::anyhow::anyhow;
+use deno_core::error::AnyError;
+use deno_core::futures::future;
+use deno_core::futures::stream;
+use deno_core::futures::StreamExt;
+use deno_core::parking_lot::Mutex;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use deno_core::ModuleSpecifier;
+use deno_runtime::permissions::Permissions;
+use deno_runtime::tokio_util::run_basic;
+use lspower::lsp;
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::sync::Arc;
+use std::time::Duration;
+use std::time::Instant;
+use tokio::sync::mpsc;
+use tokio_util::sync::CancellationToken;
+
+/// Logic to convert a test request into a set of test modules to be tested and
+/// any filters to be applied to those tests
+fn as_queue_and_filters(
+ params: &lsp_custom::TestRunRequestParams,
+ tests: &HashMap<ModuleSpecifier, TestDefinitions>,
+) -> (
+ HashSet<ModuleSpecifier>,
+ HashMap<ModuleSpecifier, TestFilter>,
+) {
+ let mut queue: HashSet<ModuleSpecifier> = HashSet::new();
+ let mut filters: HashMap<ModuleSpecifier, TestFilter> = HashMap::new();
+
+ if let Some(include) = &params.include {
+ for item in include {
+ if let Some(test_definitions) = tests.get(&item.text_document.uri) {
+ queue.insert(item.text_document.uri.clone());
+ if let Some(id) = &item.id {
+ if let Some(test) = test_definitions.get_by_id(id) {
+ let filter =
+ filters.entry(item.text_document.uri.clone()).or_default();
+ if let Some(include) = filter.maybe_include.as_mut() {
+ include.insert(test.id.clone(), test.clone());
+ } else {
+ let mut include = HashMap::new();
+ include.insert(test.id.clone(), test.clone());
+ filter.maybe_include = Some(include);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // if we didn't have any specific include filters, we assume that all modules
+ // will be tested
+ if queue.is_empty() {
+ queue.extend(tests.keys().cloned());
+ }
+
+ if let Some(exclude) = &params.exclude {
+ for item in exclude {
+ if let Some(test_definitions) = tests.get(&item.text_document.uri) {
+ if let Some(id) = &item.id {
+ // there is currently no way to filter out a specific test, so we have
+ // to ignore the exclusion
+ if item.step_id.is_none() {
+ if let Some(test) = test_definitions.get_by_id(id) {
+ let filter =
+ filters.entry(item.text_document.uri.clone()).or_default();
+ if let Some(exclude) = filter.maybe_exclude.as_mut() {
+ exclude.insert(test.id.clone(), test.clone());
+ } else {
+ let mut exclude = HashMap::new();
+ exclude.insert(test.id.clone(), test.clone());
+ filter.maybe_exclude = Some(exclude);
+ }
+ }
+ }
+ } else {
+ // the entire test module is excluded
+ queue.remove(&item.text_document.uri);
+ }
+ }
+ }
+ }
+
+ (queue, filters)
+}
+
+fn as_test_messages<S: AsRef<str>>(
+ message: S,
+ is_markdown: bool,
+) -> Vec<lsp_custom::TestMessage> {
+ let message = lsp::MarkupContent {
+ kind: if is_markdown {
+ lsp::MarkupKind::Markdown
+ } else {
+ lsp::MarkupKind::PlainText
+ },
+ value: message.as_ref().to_string(),
+ };
+ vec![lsp_custom::TestMessage {
+ message,
+ expected_output: None,
+ actual_output: None,
+ location: None,
+ }]
+}
+
+#[derive(Debug, Clone, Default, PartialEq)]
+struct TestFilter {
+ maybe_include: Option<HashMap<String, TestDefinition>>,
+ maybe_exclude: Option<HashMap<String, TestDefinition>>,
+}
+
+impl TestFilter {
+ fn as_ids(&self, test_definitions: &TestDefinitions) -> Vec<String> {
+ let ids: Vec<String> = if let Some(include) = &self.maybe_include {
+ include.keys().cloned().collect()
+ } else {
+ test_definitions
+ .discovered
+ .iter()
+ .map(|td| td.id.clone())
+ .collect()
+ };
+ if let Some(exclude) = &self.maybe_exclude {
+ ids
+ .into_iter()
+ .filter(|id| !exclude.contains_key(id))
+ .collect()
+ } else {
+ ids
+ }
+ }
+
+ /// return the filter as a JSON value, suitable for sending as a filter to the
+ /// test runner.
+ fn as_test_options(&self) -> Value {
+ let maybe_include: Option<Vec<String>> = self
+ .maybe_include
+ .as_ref()
+ .map(|inc| inc.iter().map(|(_, td)| td.name.clone()).collect());
+ let maybe_exclude: Option<Vec<String>> = self
+ .maybe_exclude
+ .as_ref()
+ .map(|ex| ex.iter().map(|(_, td)| td.name.clone()).collect());
+ json!({
+ "filter": {
+ "include": maybe_include,
+ "exclude": maybe_exclude,
+ }
+ })
+ }
+}
+
+async fn test_specifier(
+ ps: proc_state::ProcState,
+ permissions: Permissions,
+ specifier: ModuleSpecifier,
+ mode: test::TestMode,
+ channel: mpsc::UnboundedSender<test::TestEvent>,
+ token: CancellationToken,
+ options: Option<Value>,
+) -> Result<(), AnyError> {
+ if !token.is_cancelled() {
+ let mut worker = create_main_worker(
+ &ps,
+ specifier.clone(),
+ permissions,
+ vec![ops::testing::init(channel.clone())],
+ );
+
+ worker
+ .execute_script(
+ &located_script_name!(),
+ "Deno.core.enableOpCallTracing();",
+ )
+ .unwrap();
+
+ if mode != test::TestMode::Documentation {
+ worker.execute_side_module(&specifier).await?;
+ }
+
+ worker.dispatch_load_event(&located_script_name!())?;
+
+ let options = options.unwrap_or_else(|| json!({}));
+ let test_result = worker.js_runtime.execute_script(
+ &located_script_name!(),
+ &format!(r#"Deno[Deno.internal].runTests({})"#, json!(options)),
+ )?;
+
+ worker.js_runtime.resolve_value(test_result).await?;
+
+ worker.dispatch_unload_event(&located_script_name!())?;
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, Clone)]
+pub struct TestRun {
+ id: u32,
+ kind: lsp_custom::TestRunKind,
+ filters: HashMap<ModuleSpecifier, TestFilter>,
+ queue: HashSet<ModuleSpecifier>,
+ tests: Arc<Mutex<HashMap<ModuleSpecifier, TestDefinitions>>>,
+ token: CancellationToken,
+ workspace_settings: config::WorkspaceSettings,
+}
+
+impl TestRun {
+ pub fn new(
+ params: &lsp_custom::TestRunRequestParams,
+ tests: Arc<Mutex<HashMap<ModuleSpecifier, TestDefinitions>>>,
+ workspace_settings: config::WorkspaceSettings,
+ ) -> Self {
+ let (queue, filters) = {
+ let tests = tests.lock();
+ as_queue_and_filters(params, &tests)
+ };
+
+ Self {
+ id: params.id,
+ kind: params.kind.clone(),
+ filters,
+ queue,
+ tests,
+ token: CancellationToken::new(),
+ workspace_settings,
+ }
+ }
+
+ /// Provide the tests of a test run as an enqueued module which can be sent
+ /// to the client to indicate tests are enqueued for testing.
+ pub fn as_enqueued(&self) -> Vec<lsp_custom::EnqueuedTestModule> {
+ let tests = self.tests.lock();
+ self
+ .queue
+ .iter()
+ .map(|s| {
+ let ids = if let Some(test_definitions) = tests.get(s) {
+ if let Some(filter) = self.filters.get(s) {
+ filter.as_ids(test_definitions)
+ } else {
+ test_definitions
+ .discovered
+ .iter()
+ .map(|test| test.id.clone())
+ .collect()
+ }
+ } else {
+ Vec::new()
+ };
+ lsp_custom::EnqueuedTestModule {
+ text_document: lsp::TextDocumentIdentifier { uri: s.clone() },
+ ids,
+ }
+ })
+ .collect()
+ }
+
+ /// If being executed, cancel the test.
+ pub fn cancel(&self) {
+ self.token.cancel();
+ }
+
+ /// Execute the tests, dispatching progress notifications to the client.
+ pub async fn exec(
+ &self,
+ client: &Client,
+ maybe_root_uri: Option<&ModuleSpecifier>,
+ ) -> Result<(), AnyError> {
+ let args = self.get_args();
+ lsp_log!("Executing test run with arguments: {}", args.join(" "));
+ let flags = flags::flags_from_vec(args)?;
+ let ps = proc_state::ProcState::build(Arc::new(flags)).await?;
+ let permissions =
+ Permissions::from_options(&ps.flags.permissions_options());
+ test::check_specifiers(
+ &ps,
+ permissions.clone(),
+ self
+ .queue
+ .iter()
+ .map(|s| (s.clone(), test::TestMode::Executable))
+ .collect(),
+ emit::TypeLib::DenoWindow,
+ )
+ .await?;
+
+ let (sender, mut receiver) = mpsc::unbounded_channel::<test::TestEvent>();
+
+ let (concurrent_jobs, fail_fast) =
+ if let flags::DenoSubcommand::Test(test_flags) = &ps.flags.subcommand {
+ (
+ test_flags.concurrent_jobs.into(),
+ test_flags.fail_fast.map(|count| count.into()),
+ )
+ } else {
+ unreachable!("Should always be Test subcommand.");
+ };
+
+ let mut queue = self.queue.iter().collect::<Vec<&ModuleSpecifier>>();
+ queue.sort();
+
+ let join_handles = queue.into_iter().map(move |specifier| {
+ let specifier = specifier.clone();
+ let ps = ps.clone();
+ let permissions = permissions.clone();
+ let sender = sender.clone();
+ let options = self.filters.get(&specifier).map(|f| f.as_test_options());
+ let token = self.token.clone();
+
+ tokio::task::spawn_blocking(move || {
+ let future = test_specifier(
+ ps,
+ permissions,
+ specifier,
+ test::TestMode::Executable,
+ sender,
+ token,
+ options,
+ );
+
+ run_basic(future)
+ })
+ });
+
+ let join_stream = stream::iter(join_handles)
+ .buffer_unordered(concurrent_jobs)
+ .collect::<Vec<Result<Result<(), AnyError>, tokio::task::JoinError>>>();
+
+ let mut reporter: Box<dyn test::TestReporter + Send> =
+ Box::new(LspTestReporter::new(
+ self,
+ client.clone(),
+ maybe_root_uri,
+ self.tests.clone(),
+ ));
+
+ let handler = {
+ tokio::task::spawn(async move {
+ let earlier = Instant::now();
+ let mut summary = test::TestSummary::new();
+ let mut used_only = false;
+
+ while let Some(event) = receiver.recv().await {
+ match event {
+ test::TestEvent::Plan(plan) => {
+ summary.total += plan.total;
+ summary.filtered_out += plan.filtered_out;
+
+ if plan.used_only {
+ used_only = true;
+ }
+
+ reporter.report_plan(&plan);
+ }
+ test::TestEvent::Wait(description) => {
+ reporter.report_wait(&description);
+ }
+ test::TestEvent::Output(output) => {
+ reporter.report_output(&output);
+ }
+ test::TestEvent::Result(description, result, elapsed) => {
+ match &result {
+ test::TestResult::Ok => summary.passed += 1,
+ test::TestResult::Ignored => summary.ignored += 1,
+ test::TestResult::Failed(error) => {
+ summary.failed += 1;
+ summary.failures.push((description.clone(), error.clone()));
+ }
+ }
+
+ reporter.report_result(&description, &result, elapsed);
+ }
+ test::TestEvent::StepWait(description) => {
+ reporter.report_step_wait(&description);
+ }
+ test::TestEvent::StepResult(description, result, duration) => {
+ match &result {
+ test::TestStepResult::Ok => {
+ summary.passed_steps += 1;
+ }
+ test::TestStepResult::Ignored => {
+ summary.ignored_steps += 1;
+ }
+ test::TestStepResult::Failed(_) => {
+ summary.failed_steps += 1;
+ }
+ test::TestStepResult::Pending(_) => {
+ summary.pending_steps += 1;
+ }
+ }
+ reporter.report_step_result(&description, &result, duration);
+ }
+ }
+
+ if let Some(count) = fail_fast {
+ if summary.failed >= count {
+ break;
+ }
+ }
+ }
+
+ let elapsed = Instant::now().duration_since(earlier);
+ reporter.report_summary(&summary, &elapsed);
+
+ if used_only {
+ return Err(anyhow!(
+ "Test failed because the \"only\" option was used"
+ ));
+ }
+
+ if summary.failed > 0 {
+ return Err(anyhow!("Test failed"));
+ }
+
+ Ok(())
+ })
+ };
+
+ let (join_results, result) = future::join(join_stream, handler).await;
+
+ // propagate any errors
+ for join_result in join_results {
+ join_result??;
+ }
+
+ result??;
+
+ Ok(())
+ }
+
+ fn get_args(&self) -> Vec<&str> {
+ let mut args = vec!["deno", "test"];
+ args.extend(
+ self
+ .workspace_settings
+ .testing
+ .args
+ .iter()
+ .map(|s| s.as_str()),
+ );
+ if self.workspace_settings.unstable && !args.contains(&"--unstable") {
+ args.push("--unstable");
+ }
+ if let Some(config) = &self.workspace_settings.config {
+ if !args.contains(&"--config") && !args.contains(&"-c") {
+ args.push("--config");
+ args.push(config.as_str());
+ }
+ }
+ if let Some(import_map) = &self.workspace_settings.import_map {
+ if !args.contains(&"--import-map") {
+ args.push("--import-map");
+ args.push(import_map.as_str());
+ }
+ }
+ if self.kind == lsp_custom::TestRunKind::Debug
+ && !args.contains(&"--inspect")
+ && !args.contains(&"--inspect-brk")
+ {
+ args.push("--inspect");
+ }
+ args
+ }
+}
+
+#[derive(Debug, PartialEq)]
+enum TestOrTestStepDescription {
+ TestDescription(test::TestDescription),
+ TestStepDescription(test::TestStepDescription),
+}
+
+impl From<&test::TestDescription> for TestOrTestStepDescription {
+ fn from(desc: &test::TestDescription) -> Self {
+ Self::TestDescription(desc.clone())
+ }
+}
+
+impl From<&test::TestStepDescription> for TestOrTestStepDescription {
+ fn from(desc: &test::TestStepDescription) -> Self {
+ Self::TestStepDescription(desc.clone())
+ }
+}
+
+impl From<&TestOrTestStepDescription> for lsp_custom::TestIdentifier {
+ fn from(desc: &TestOrTestStepDescription) -> lsp_custom::TestIdentifier {
+ match desc {
+ TestOrTestStepDescription::TestDescription(test_desc) => test_desc.into(),
+ TestOrTestStepDescription::TestStepDescription(test_step_desc) => {
+ test_step_desc.into()
+ }
+ }
+ }
+}
+
+impl From<&TestOrTestStepDescription> for lsp_custom::TestData {
+ fn from(desc: &TestOrTestStepDescription) -> Self {
+ match desc {
+ TestOrTestStepDescription::TestDescription(desc) => desc.into(),
+ TestOrTestStepDescription::TestStepDescription(desc) => desc.into(),
+ }
+ }
+}
+
+impl From<&test::TestDescription> for lsp_custom::TestData {
+ fn from(desc: &test::TestDescription) -> Self {
+ let id = checksum::gen(&[desc.origin.as_bytes(), desc.name.as_bytes()]);
+
+ Self {
+ id,
+ label: desc.name.clone(),
+ steps: Default::default(),
+ range: None,
+ }
+ }
+}
+
+impl From<&test::TestDescription> for lsp_custom::TestIdentifier {
+ fn from(desc: &test::TestDescription) -> Self {
+ let uri = ModuleSpecifier::parse(&desc.origin).unwrap();
+ let id = Some(checksum::gen(&[
+ desc.origin.as_bytes(),
+ desc.name.as_bytes(),
+ ]));
+
+ Self {
+ text_document: lsp::TextDocumentIdentifier { uri },
+ id,
+ step_id: None,
+ }
+ }
+}
+
+impl From<&test::TestStepDescription> for lsp_custom::TestData {
+ fn from(desc: &test::TestStepDescription) -> Self {
+ let id = checksum::gen(&[
+ desc.test.origin.as_bytes(),
+ &desc.level.to_be_bytes(),
+ desc.name.as_bytes(),
+ ]);
+
+ Self {
+ id,
+ label: desc.name.clone(),
+ steps: Default::default(),
+ range: None,
+ }
+ }
+}
+
+impl From<&test::TestStepDescription> for lsp_custom::TestIdentifier {
+ fn from(desc: &test::TestStepDescription) -> Self {
+ let uri = ModuleSpecifier::parse(&desc.test.origin).unwrap();
+ let id = Some(checksum::gen(&[
+ desc.test.origin.as_bytes(),
+ desc.test.name.as_bytes(),
+ ]));
+ let step_id = Some(checksum::gen(&[
+ desc.test.origin.as_bytes(),
+ &desc.level.to_be_bytes(),
+ desc.name.as_bytes(),
+ ]));
+
+ Self {
+ text_document: lsp::TextDocumentIdentifier { uri },
+ id,
+ step_id,
+ }
+ }
+}
+
+struct LspTestReporter {
+ client: Client,
+ current_origin: Option<String>,
+ maybe_root_uri: Option<ModuleSpecifier>,
+ id: u32,
+ stack: HashMap<String, Vec<TestOrTestStepDescription>>,
+ tests: Arc<Mutex<HashMap<ModuleSpecifier, TestDefinitions>>>,
+}
+
+impl LspTestReporter {
+ fn new(
+ run: &TestRun,
+ client: Client,
+ maybe_root_uri: Option<&ModuleSpecifier>,
+ tests: Arc<Mutex<HashMap<ModuleSpecifier, TestDefinitions>>>,
+ ) -> Self {
+ Self {
+ client,
+ current_origin: None,
+ maybe_root_uri: maybe_root_uri.cloned(),
+ id: run.id,
+ stack: HashMap::new(),
+ tests,
+ }
+ }
+
+ fn add_step(&self, desc: &test::TestStepDescription) {
+ if let Ok(specifier) = ModuleSpecifier::parse(&desc.test.origin) {
+ let mut tests = self.tests.lock();
+ let entry =
+ tests
+ .entry(specifier.clone())
+ .or_insert_with(|| TestDefinitions {
+ discovered: Default::default(),
+ injected: Default::default(),
+ script_version: "1".to_string(),
+ });
+ let mut prev: lsp_custom::TestData = desc.into();
+ if let Some(stack) = self.stack.get(&desc.test.origin) {
+ for item in stack.iter().rev() {
+ let mut data: lsp_custom::TestData = item.into();
+ data.steps = Some(vec![prev]);
+ prev = data;
+ }
+ entry.injected.push(prev.clone());
+ let label = if let Some(root) = &self.maybe_root_uri {
+ specifier.as_str().replace(root.as_str(), "")
+ } else {
+ specifier
+ .path_segments()
+ .and_then(|s| s.last().map(|s| s.to_string()))
+ .unwrap_or_else(|| "<unknown>".to_string())
+ };
+ self
+ .client
+ .send_test_notification(TestingNotification::Module(
+ lsp_custom::TestModuleNotificationParams {
+ text_document: lsp::TextDocumentIdentifier { uri: specifier },
+ kind: lsp_custom::TestModuleNotificationKind::Insert,
+ label,
+ tests: vec![prev],
+ },
+ ));
+ }
+ }
+ }
+
+ /// Add a test which is being reported from the test runner but was not
+ /// statically identified
+ fn add_test(&self, desc: &test::TestDescription) {
+ if let Ok(specifier) = ModuleSpecifier::parse(&desc.origin) {
+ let mut tests = self.tests.lock();
+ let entry =
+ tests
+ .entry(specifier.clone())
+ .or_insert_with(|| TestDefinitions {
+ discovered: Default::default(),
+ injected: Default::default(),
+ script_version: "1".to_string(),
+ });
+ entry.injected.push(desc.into());
+ let label = if let Some(root) = &self.maybe_root_uri {
+ specifier.as_str().replace(root.as_str(), "")
+ } else {
+ specifier
+ .path_segments()
+ .and_then(|s| s.last().map(|s| s.to_string()))
+ .unwrap_or_else(|| "<unknown>".to_string())
+ };
+ self
+ .client
+ .send_test_notification(TestingNotification::Module(
+ lsp_custom::TestModuleNotificationParams {
+ text_document: lsp::TextDocumentIdentifier { uri: specifier },
+ kind: lsp_custom::TestModuleNotificationKind::Insert,
+ label,
+ tests: vec![desc.into()],
+ },
+ ));
+ }
+ }
+
+ fn progress(&self, message: lsp_custom::TestRunProgressMessage) {
+ self
+ .client
+ .send_test_notification(TestingNotification::Progress(
+ lsp_custom::TestRunProgressParams {
+ id: self.id,
+ message,
+ },
+ ));
+ }
+
+ fn includes_step(&self, desc: &test::TestStepDescription) -> bool {
+ if let Ok(specifier) = ModuleSpecifier::parse(&desc.test.origin) {
+ let tests = self.tests.lock();
+ if let Some(test_definitions) = tests.get(&specifier) {
+ return test_definitions
+ .get_step_by_name(&desc.test.name, desc.level, &desc.name)
+ .is_some();
+ }
+ }
+ false
+ }
+
+ fn includes_test(&self, desc: &test::TestDescription) -> bool {
+ if let Ok(specifier) = ModuleSpecifier::parse(&desc.origin) {
+ let tests = self.tests.lock();
+ if let Some(test_definitions) = tests.get(&specifier) {
+ return test_definitions.get_by_name(&desc.name).is_some();
+ }
+ }
+ false
+ }
+}
+
+impl test::TestReporter for LspTestReporter {
+ fn report_plan(&mut self, _plan: &test::TestPlan) {
+ // there is nothing to do on report_plan
+ }
+
+ fn report_wait(&mut self, desc: &test::TestDescription) {
+ if !self.includes_test(desc) {
+ self.add_test(desc);
+ }
+ self.current_origin = Some(desc.origin.clone());
+ let test: lsp_custom::TestIdentifier = desc.into();
+ let stack = self.stack.entry(desc.origin.clone()).or_default();
+ assert!(stack.is_empty());
+ stack.push(desc.into());
+ self.progress(lsp_custom::TestRunProgressMessage::Started { test });
+ }
+
+ fn report_output(&mut self, output: &test::TestOutput) {
+ let test = self.current_origin.as_ref().and_then(|origin| {
+ self
+ .stack
+ .get(origin)
+ .and_then(|v| v.last().map(|td| td.into()))
+ });
+ match output {
+ test::TestOutput::Console(value) => {
+ self.progress(lsp_custom::TestRunProgressMessage::Output {
+ value: value.replace('\n', "\r\n"),
+ test,
+ // TODO(@kitsonk) test output should include a location
+ location: None,
+ })
+ }
+ }
+ }
+
+ fn report_result(
+ &mut self,
+ desc: &test::TestDescription,
+ result: &test::TestResult,
+ elapsed: u64,
+ ) {
+ let stack = self.stack.entry(desc.origin.clone()).or_default();
+ assert_eq!(stack.len(), 1);
+ assert_eq!(stack.pop(), Some(desc.into()));
+ self.current_origin = None;
+ match result {
+ test::TestResult::Ok => {
+ self.progress(lsp_custom::TestRunProgressMessage::Passed {
+ test: desc.into(),
+ duration: Some(elapsed as u32),
+ })
+ }
+ test::TestResult::Ignored => {
+ self.progress(lsp_custom::TestRunProgressMessage::Skipped {
+ test: desc.into(),
+ })
+ }
+ test::TestResult::Failed(message) => {
+ self.progress(lsp_custom::TestRunProgressMessage::Failed {
+ test: desc.into(),
+ messages: as_test_messages(message, false),
+ duration: Some(elapsed as u32),
+ })
+ }
+ }
+ }
+
+ fn report_step_wait(&mut self, desc: &test::TestStepDescription) {
+ if !self.includes_step(desc) {
+ self.add_step(desc);
+ }
+ let test: lsp_custom::TestIdentifier = desc.into();
+ let stack = self.stack.entry(desc.test.origin.clone()).or_default();
+ self.current_origin = Some(desc.test.origin.clone());
+ assert!(!stack.is_empty());
+ stack.push(desc.into());
+ self.progress(lsp_custom::TestRunProgressMessage::Started { test });
+ }
+
+ fn report_step_result(
+ &mut self,
+ desc: &test::TestStepDescription,
+ result: &test::TestStepResult,
+ elapsed: u64,
+ ) {
+ let stack = self.stack.entry(desc.test.origin.clone()).or_default();
+ assert_eq!(stack.pop(), Some(desc.into()));
+ match result {
+ test::TestStepResult::Ok => {
+ self.progress(lsp_custom::TestRunProgressMessage::Passed {
+ test: desc.into(),
+ duration: Some(elapsed as u32),
+ })
+ }
+ test::TestStepResult::Ignored => {
+ self.progress(lsp_custom::TestRunProgressMessage::Skipped {
+ test: desc.into(),
+ })
+ }
+ test::TestStepResult::Failed(message) => {
+ let messages = if let Some(message) = message {
+ as_test_messages(message, false)
+ } else {
+ vec![]
+ };
+ self.progress(lsp_custom::TestRunProgressMessage::Failed {
+ test: desc.into(),
+ messages,
+ duration: Some(elapsed as u32),
+ })
+ }
+ test::TestStepResult::Pending(_) => {
+ self.progress(lsp_custom::TestRunProgressMessage::Enqueued {
+ test: desc.into(),
+ })
+ }
+ }
+ }
+
+ fn report_summary(
+ &mut self,
+ _summary: &test::TestSummary,
+ _elapsed: &Duration,
+ ) {
+ // there is nothing to do on report_summary
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::lsp::testing::collectors::tests::new_span;
+
+ #[test]
+ fn test_as_queue_and_filters() {
+ let specifier = ModuleSpecifier::parse("file:///a/file.ts").unwrap();
+ let params = lsp_custom::TestRunRequestParams {
+ id: 1,
+ kind: lsp_custom::TestRunKind::Run,
+ include: Some(vec![lsp_custom::TestIdentifier {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: specifier.clone(),
+ },
+ id: None,
+ step_id: None,
+ }]),
+ exclude: Some(vec![lsp_custom::TestIdentifier {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: specifier.clone(),
+ },
+ id: Some(
+ "69d9fe87f64f5b66cb8b631d4fd2064e8224b8715a049be54276c42189ff8f9f"
+ .to_string(),
+ ),
+ step_id: None,
+ }]),
+ };
+ let mut tests = HashMap::new();
+ let test_def_a = TestDefinition {
+ id: "0b7c6bf3cd617018d33a1bf982a08fe088c5bb54fcd5eb9e802e7c137ec1af94"
+ .to_string(),
+ level: 0,
+ name: "test a".to_string(),
+ span: new_span(420, 424, 1),
+ steps: None,
+ };
+ let test_def_b = TestDefinition {
+ id: "69d9fe87f64f5b66cb8b631d4fd2064e8224b8715a049be54276c42189ff8f9f"
+ .to_string(),
+ level: 0,
+ name: "test b".to_string(),
+ span: new_span(480, 481, 1),
+ steps: None,
+ };
+ let test_definitions = TestDefinitions {
+ discovered: vec![test_def_a, test_def_b.clone()],
+ injected: vec![],
+ script_version: "1".to_string(),
+ };
+ tests.insert(specifier.clone(), test_definitions.clone());
+ let (queue, filters) = as_queue_and_filters(&params, &tests);
+ assert_eq!(json!(queue), json!([specifier]));
+ let mut exclude = HashMap::new();
+ exclude.insert(
+ "69d9fe87f64f5b66cb8b631d4fd2064e8224b8715a049be54276c42189ff8f9f"
+ .to_string(),
+ test_def_b,
+ );
+ let maybe_filter = filters.get(&specifier);
+ assert!(maybe_filter.is_some());
+ let filter = maybe_filter.unwrap();
+ assert_eq!(
+ filter,
+ &TestFilter {
+ maybe_include: None,
+ maybe_exclude: Some(exclude),
+ }
+ );
+ assert_eq!(
+ filter.as_ids(&test_definitions),
+ vec![
+ "0b7c6bf3cd617018d33a1bf982a08fe088c5bb54fcd5eb9e802e7c137ec1af94"
+ .to_string()
+ ]
+ );
+ assert_eq!(
+ filter.as_test_options(),
+ json!({
+ "filter": {
+ "include": null,
+ "exclude": vec!["test b"],
+ }
+ })
+ );
+ }
+}
diff --git a/cli/lsp/testing/lsp_custom.rs b/cli/lsp/testing/lsp_custom.rs
new file mode 100644
index 000000000..c1182b04e
--- /dev/null
+++ b/cli/lsp/testing/lsp_custom.rs
@@ -0,0 +1,186 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::serde::Deserialize;
+use deno_core::serde::Serialize;
+use lspower::lsp;
+
+pub const TEST_RUN_CANCEL_REQUEST: &str = "deno/testRunCancel";
+pub const TEST_RUN_REQUEST: &str = "deno/testRun";
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EnqueuedTestModule {
+ pub text_document: lsp::TextDocumentIdentifier,
+ pub ids: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestData {
+ /// The unique ID of the test
+ pub id: String,
+ /// The human readable test to display for the test.
+ pub label: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub steps: Option<Vec<TestData>>,
+ /// The range where the test is located.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub range: Option<lsp::Range>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum TestModuleNotificationKind {
+ /// The test module notification represents an insertion of tests, not
+ /// replacement of the test children.
+ Insert,
+ /// The test module notification represents a replacement of any tests within
+ /// the test module.
+ Replace,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestModuleNotificationParams {
+ /// The text document that the notification relates to.
+ pub text_document: lsp::TextDocumentIdentifier,
+ /// Indicates what kind of notification this represents.
+ pub kind: TestModuleNotificationKind,
+ /// The human readable text to display for the test module.
+ pub label: String,
+ /// The tests identified in the module.
+ pub tests: Vec<TestData>,
+}
+
+pub enum TestModuleNotification {}
+
+impl lsp::notification::Notification for TestModuleNotification {
+ type Params = TestModuleNotificationParams;
+
+ const METHOD: &'static str = "deno/testModule";
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestModuleDeleteNotificationParams {
+ /// The text document that the notification relates to.
+ pub text_document: lsp::TextDocumentIdentifier,
+}
+
+pub enum TestModuleDeleteNotification {}
+
+impl lsp::notification::Notification for TestModuleDeleteNotification {
+ type Params = TestModuleDeleteNotificationParams;
+
+ const METHOD: &'static str = "deno/testModuleDelete";
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum TestRunKind {
+ // The run profile is just to execute the tests
+ Run,
+ // The tests should be run and debugged, currently not implemented
+ Debug,
+ // The tests should be run, collecting and reporting coverage information,
+ // currently not implemented
+ Coverage,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestRunRequestParams {
+ pub id: u32,
+ pub kind: TestRunKind,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub exclude: Option<Vec<TestIdentifier>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub include: Option<Vec<TestIdentifier>>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestRunCancelParams {
+ pub id: u32,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestRunProgressParams {
+ pub id: u32,
+ pub message: TestRunProgressMessage,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct TestIdentifier {
+ /// The module identifier which contains the test.
+ pub text_document: lsp::TextDocumentIdentifier,
+ /// An optional string identifying the individual test. If not present, then
+ /// it identifies all the tests associated with the module.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id: Option<String>,
+ /// An optional structure identifying a step of the test. If not present, then
+ /// no step is identified.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub step_id: Option<String>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase", tag = "type")]
+pub enum TestRunProgressMessage {
+ Enqueued {
+ test: TestIdentifier,
+ },
+ Started {
+ test: TestIdentifier,
+ },
+ Skipped {
+ test: TestIdentifier,
+ },
+ Failed {
+ test: TestIdentifier,
+ messages: Vec<TestMessage>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ duration: Option<u32>,
+ },
+ Errored {
+ test: TestIdentifier,
+ messages: Vec<TestMessage>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ duration: Option<u32>,
+ },
+ Passed {
+ test: TestIdentifier,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ duration: Option<u32>,
+ },
+ Output {
+ value: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ test: Option<TestIdentifier>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ location: Option<lsp::Location>,
+ },
+ End,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TestMessage {
+ pub message: lsp::MarkupContent,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expected_output: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub actual_output: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub location: Option<lsp::Location>,
+}
+
+pub enum TestRunProgressNotification {}
+
+impl lsp::notification::Notification for TestRunProgressNotification {
+ type Params = TestRunProgressParams;
+
+ const METHOD: &'static str = "deno/testRunProgress";
+}
diff --git a/cli/lsp/testing/mod.rs b/cli/lsp/testing/mod.rs
new file mode 100644
index 000000000..cbd49724e
--- /dev/null
+++ b/cli/lsp/testing/mod.rs
@@ -0,0 +1,11 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+mod collectors;
+mod definitions;
+mod execution;
+pub mod lsp_custom;
+mod server;
+
+pub use lsp_custom::TEST_RUN_CANCEL_REQUEST;
+pub use lsp_custom::TEST_RUN_REQUEST;
+pub use server::TestServer;
diff --git a/cli/lsp/testing/server.rs b/cli/lsp/testing/server.rs
new file mode 100644
index 000000000..b176fea68
--- /dev/null
+++ b/cli/lsp/testing/server.rs
@@ -0,0 +1,219 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use super::collectors::TestCollector;
+use super::definitions::TestDefinitions;
+use super::execution::TestRun;
+use super::lsp_custom;
+
+use crate::lsp::client::Client;
+use crate::lsp::client::TestingNotification;
+use crate::lsp::config;
+use crate::lsp::language_server::StateSnapshot;
+use crate::lsp::performance::Performance;
+
+use deno_ast::swc::visit::VisitWith;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::Mutex;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use deno_core::ModuleSpecifier;
+use deno_runtime::tokio_util::create_basic_runtime;
+use lspower::jsonrpc::Error as LspError;
+use lspower::jsonrpc::Result as LspResult;
+use lspower::lsp;
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::sync::Arc;
+use std::thread;
+use tokio::sync::mpsc;
+
+fn as_delete_notification(uri: ModuleSpecifier) -> TestingNotification {
+ TestingNotification::DeleteModule(
+ lsp_custom::TestModuleDeleteNotificationParams {
+ text_document: lsp::TextDocumentIdentifier { uri },
+ },
+ )
+}
+
+/// The main structure which handles requests and sends notifications related
+/// to the Testing API.
+#[derive(Debug)]
+pub struct TestServer {
+ client: Client,
+ performance: Arc<Performance>,
+ /// A channel for handling run requests from the client
+ run_channel: mpsc::UnboundedSender<u32>,
+ /// A map of run ids to test runs
+ runs: Arc<Mutex<HashMap<u32, TestRun>>>,
+ /// Tests that are discovered from a versioned document
+ tests: Arc<Mutex<HashMap<ModuleSpecifier, TestDefinitions>>>,
+ /// A channel for requesting that changes to documents be statically analyzed
+ /// for tests
+ update_channel: mpsc::UnboundedSender<Arc<StateSnapshot>>,
+}
+
+impl TestServer {
+ pub fn new(
+ client: Client,
+ performance: Arc<Performance>,
+ maybe_root_uri: Option<ModuleSpecifier>,
+ ) -> Self {
+ let tests: Arc<Mutex<HashMap<ModuleSpecifier, TestDefinitions>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+
+ let (update_channel, mut update_rx) =
+ mpsc::unbounded_channel::<Arc<StateSnapshot>>();
+ let (run_channel, mut run_rx) = mpsc::unbounded_channel::<u32>();
+
+ let server = Self {
+ client,
+ performance,
+ run_channel,
+ runs: Default::default(),
+ tests,
+ update_channel,
+ };
+
+ let tests = server.tests.clone();
+ let client = server.client.clone();
+ let performance = server.performance.clone();
+ let mru = maybe_root_uri.clone();
+ let _update_join_handle = thread::spawn(move || {
+ let runtime = create_basic_runtime();
+
+ runtime.block_on(async {
+ loop {
+ match update_rx.recv().await {
+ None => break,
+ Some(snapshot) => {
+ let mark = performance.mark("testing_update", None::<()>);
+ let mut tests = tests.lock();
+ // we create a list of test modules we currently are tracking
+ // eliminating any we go over when iterating over the document
+ let mut keys: HashSet<ModuleSpecifier> =
+ tests.keys().cloned().collect();
+ for document in snapshot.documents.documents(false, true) {
+ let specifier = document.specifier();
+ keys.remove(specifier);
+ let script_version = document.script_version();
+ let valid = if let Some(test) = tests.get(specifier) {
+ test.script_version == script_version
+ } else {
+ false
+ };
+ if !valid {
+ if let Some(Ok(parsed_source)) =
+ document.maybe_parsed_source()
+ {
+ let mut collector = TestCollector::new(specifier.clone());
+ parsed_source.module().visit_with(&mut collector);
+ let test_definitions = TestDefinitions {
+ discovered: collector.take(),
+ injected: Default::default(),
+ script_version,
+ };
+ if !test_definitions.discovered.is_empty() {
+ client.send_test_notification(
+ test_definitions.as_notification(
+ specifier,
+ mru.as_ref(),
+ parsed_source.source(),
+ ),
+ );
+ }
+ tests.insert(specifier.clone(), test_definitions);
+ }
+ }
+ }
+ for key in keys {
+ client.send_test_notification(as_delete_notification(key));
+ }
+ performance.measure(mark);
+ }
+ }
+ }
+ })
+ });
+
+ let client = server.client.clone();
+ let runs = server.runs.clone();
+ let _run_join_handle = thread::spawn(move || {
+ let runtime = create_basic_runtime();
+
+ runtime.block_on(async {
+ loop {
+ match run_rx.recv().await {
+ None => break,
+ Some(id) => {
+ let maybe_run = {
+ let runs = runs.lock();
+ runs.get(&id).cloned()
+ };
+ if let Some(run) = maybe_run {
+ match run.exec(&client, maybe_root_uri.as_ref()).await {
+ Ok(_) => (),
+ Err(err) => {
+ client.show_message(lsp::MessageType::ERROR, err).await;
+ }
+ }
+ client.send_test_notification(TestingNotification::Progress(
+ lsp_custom::TestRunProgressParams {
+ id,
+ message: lsp_custom::TestRunProgressMessage::End,
+ },
+ ));
+ runs.lock().remove(&id);
+ }
+ }
+ }
+ }
+ })
+ });
+
+ server
+ }
+
+ fn enqueue_run(&self, id: u32) -> Result<(), AnyError> {
+ self.run_channel.send(id).map_err(|err| err.into())
+ }
+
+ /// A request from the client to cancel a test run.
+ pub fn run_cancel_request(
+ &self,
+ params: lsp_custom::TestRunCancelParams,
+ ) -> LspResult<Option<Value>> {
+ if let Some(run) = self.runs.lock().get(&params.id) {
+ run.cancel();
+ Ok(Some(json!(true)))
+ } else {
+ Ok(Some(json!(false)))
+ }
+ }
+
+ /// A request from the client to start a test run.
+ pub fn run_request(
+ &self,
+ params: lsp_custom::TestRunRequestParams,
+ workspace_settings: config::WorkspaceSettings,
+ ) -> LspResult<Option<Value>> {
+ let test_run =
+ { TestRun::new(&params, self.tests.clone(), workspace_settings) };
+ let enqueued = test_run.as_enqueued();
+ {
+ let mut runs = self.runs.lock();
+ runs.insert(params.id, test_run);
+ }
+ self.enqueue_run(params.id).map_err(|err| {
+ log::error!("cannot enqueue run: {}", err);
+ LspError::internal_error()
+ })?;
+ Ok(Some(json!({ "enqueued": enqueued })))
+ }
+
+ pub(crate) fn update(
+ &self,
+ snapshot: Arc<StateSnapshot>,
+ ) -> Result<(), AnyError> {
+ self.update_channel.send(snapshot).map_err(|err| err.into())
+ }
+}
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 63695634a..784dc8263 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -38,7 +38,7 @@ fn load_fixture_str(path: &str) -> String {
fn init(init_path: &str) -> LspClient {
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", load_fixture(init_path))
.unwrap();
@@ -247,7 +247,7 @@ fn lsp_init_tsconfig() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -290,7 +290,7 @@ fn lsp_tsconfig_types() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -350,7 +350,7 @@ fn lsp_triple_slash_types() {
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -397,7 +397,7 @@ fn lsp_import_map() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -516,7 +516,7 @@ fn lsp_import_map_config_file() {
.unwrap();
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -599,7 +599,7 @@ fn lsp_deno_task() {
params.root_uri = Some(Url::from_file_path(workspace_root).unwrap());
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -725,7 +725,7 @@ fn lsp_import_map_import_completions() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -1085,7 +1085,7 @@ fn lsp_workspace_enable_paths() {
}]);
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -2240,7 +2240,7 @@ fn lsp_format_exclude_with_config() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -2292,7 +2292,7 @@ fn lsp_format_exclude_default_config() {
params.root_uri = Some(Url::from_file_path(workspace_root.clone()).unwrap());
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -3824,7 +3824,7 @@ fn lsp_cache_location() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -3945,7 +3945,7 @@ fn lsp_tls_cert() {
params.root_uri = Some(Url::from_file_path(testdata_path()).unwrap());
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -4426,7 +4426,7 @@ fn lsp_performance() {
.unwrap();
assert!(maybe_err.is_none());
if let Some(res) = maybe_res {
- assert_eq!(res.averages.len(), 13);
+ assert_eq!(res.averages.len(), 14);
} else {
panic!("unexpected result");
}
@@ -4622,7 +4622,7 @@ fn lsp_format_with_config() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -5101,7 +5101,7 @@ fn lsp_lint_with_config() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -5134,7 +5134,7 @@ fn lsp_lint_exclude_with_config() {
}
let deno_exe = deno_exe_path();
- let mut client = LspClient::new(&deno_exe).unwrap();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
@@ -5236,3 +5236,230 @@ export function B() {
);
shutdown(&mut client);
}
+
+#[derive(Debug, Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct TestData {
+ id: String,
+ label: String,
+ steps: Option<Vec<TestData>>,
+ range: Option<lsp::Range>,
+}
+
+#[derive(Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+enum TestModuleNotificationKind {
+ Insert,
+ Replace,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct TestModuleNotificationParams {
+ text_document: lsp::TextDocumentIdentifier,
+ kind: TestModuleNotificationKind,
+ label: String,
+ tests: Vec<TestData>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct EnqueuedTestModule {
+ text_document: lsp::TextDocumentIdentifier,
+ ids: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct TestRunResponseParams {
+ enqueued: Vec<EnqueuedTestModule>,
+}
+
+#[test]
+fn lsp_testing_api() {
+ let mut params: lsp::InitializeParams =
+ serde_json::from_value(load_fixture("initialize_params.json")).unwrap();
+ let temp_dir = TempDir::new().unwrap();
+
+ let root_specifier =
+ ensure_directory_specifier(Url::from_file_path(temp_dir.path()).unwrap());
+
+ let module_path = temp_dir.path().join("./test.ts");
+ let specifier = ModuleSpecifier::from_file_path(&module_path).unwrap();
+ let contents = r#"
+Deno.test({
+ name: "test a",
+ fn() {
+ console.log("test a");
+ }
+});
+"#;
+ fs::write(&module_path, &contents).unwrap();
+ fs::write(temp_dir.path().join("./deno.jsonc"), r#"{}"#).unwrap();
+
+ params.root_uri = Some(root_specifier);
+
+ let deno_exe = deno_exe_path();
+ let mut client = LspClient::new(&deno_exe, false).unwrap();
+ client
+ .write_request::<_, _, Value>("initialize", params)
+ .unwrap();
+
+ client.write_notification("initialized", json!({})).unwrap();
+
+ client
+ .write_notification(
+ "textDocument/didOpen",
+ json!({
+ "textDocument": {
+ "uri": specifier,
+ "languageId": "typescript",
+ "version": 1,
+ "text": contents,
+ }
+ }),
+ )
+ .unwrap();
+
+ handle_configuration_request(
+ &mut client,
+ json!([{
+ "enable": true,
+ "codeLens": {
+ "test": true
+ }
+ }]),
+ );
+
+ for _ in 0..4 {
+ let result = client.read_notification::<Value>();
+ assert!(result.is_ok());
+ let (method, notification) = result.unwrap();
+ if method.as_str() == "deno/testModule" {
+ let params: TestModuleNotificationParams =
+ serde_json::from_value(notification.unwrap()).unwrap();
+ assert_eq!(params.text_document.uri, specifier);
+ assert_eq!(params.kind, TestModuleNotificationKind::Replace);
+ assert_eq!(params.label, "test.ts");
+ assert_eq!(params.tests.len(), 1);
+ let test = &params.tests[0];
+ assert_eq!(test.label, "test a");
+ assert!(test.steps.is_none());
+ assert_eq!(
+ test.range,
+ Some(lsp::Range {
+ start: lsp::Position {
+ line: 1,
+ character: 5,
+ },
+ end: lsp::Position {
+ line: 1,
+ character: 9,
+ }
+ })
+ );
+ }
+ }
+
+ let (maybe_res, maybe_err) = client
+ .write_request::<_, _, TestRunResponseParams>(
+ "deno/testRun",
+ json!({
+ "id": 1,
+ "kind": "run",
+ }),
+ )
+ .unwrap();
+ assert!(maybe_err.is_none());
+ assert!(maybe_res.is_some());
+ let res = maybe_res.unwrap();
+ assert_eq!(res.enqueued.len(), 1);
+ assert_eq!(res.enqueued[0].text_document.uri, specifier);
+ assert_eq!(res.enqueued[0].ids.len(), 1);
+ let id = res.enqueued[0].ids[0].clone();
+
+ let res = client.read_notification::<Value>();
+ assert!(res.is_ok());
+ let (method, notification) = res.unwrap();
+ assert_eq!(method, "deno/testRunProgress");
+ assert_eq!(
+ notification,
+ Some(json!({
+ "id": 1,
+ "message": {
+ "type": "started",
+ "test": {
+ "textDocument": {
+ "uri": specifier,
+ },
+ "id": id,
+ },
+ }
+ }))
+ );
+
+ let res = client.read_notification::<Value>();
+ assert!(res.is_ok());
+ let (method, notification) = res.unwrap();
+ assert_eq!(method, "deno/testRunProgress");
+ assert_eq!(
+ notification,
+ Some(json!({
+ "id": 1,
+ "message": {
+ "type": "output",
+ "value": "test a\r\n",
+ "test": {
+ "textDocument": {
+ "uri": specifier,
+ },
+ "id": id,
+ },
+ }
+ }))
+ );
+
+ let res = client.read_notification::<Value>();
+ assert!(res.is_ok());
+ let (method, notification) = res.unwrap();
+ assert_eq!(method, "deno/testRunProgress");
+ let notification = notification.unwrap();
+ let obj = notification.as_object().unwrap();
+ assert_eq!(obj.get("id"), Some(&json!(1)));
+ let message = obj.get("message").unwrap().as_object().unwrap();
+ match message.get("type").and_then(|v| v.as_str()) {
+ Some("passed") => {
+ assert_eq!(
+ message.get("test"),
+ Some(&json!({
+ "textDocument": {
+ "uri": specifier
+ },
+ "id": id,
+ }))
+ );
+ assert!(message.contains_key("duration"));
+
+ let res = client.read_notification::<Value>();
+ assert!(res.is_ok());
+ let (method, notification) = res.unwrap();
+ assert_eq!(method, "deno/testRunProgress");
+ assert_eq!(
+ notification,
+ Some(json!({
+ "id": 1,
+ "message": {
+ "type": "end",
+ }
+ }))
+ );
+ }
+ // sometimes on windows, the messages come out of order, but it actually is
+ // working, so if we do get the end before the passed, we will simply let
+ // the test pass
+ Some("end") => (),
+ _ => panic!("unexpected message {}", json!(notification)),
+ }
+
+ shutdown(&mut client);
+}
diff --git a/cli/tests/testdata/lsp/initialize_params.json b/cli/tests/testdata/lsp/initialize_params.json
index 9fd197fe4..b076f3b17 100644
--- a/cli/tests/testdata/lsp/initialize_params.json
+++ b/cli/tests/testdata/lsp/initialize_params.json
@@ -14,7 +14,7 @@
"references": true,
"test": true
},
- "config": "",
+ "config": null,
"importMap": null,
"lint": true,
"suggest": {
@@ -26,6 +26,12 @@
"hosts": {}
}
},
+ "testing": {
+ "args": [
+ "--allow-all"
+ ],
+ "enable": true
+ },
"tlsCertificate": null,
"unsafelyIgnoreCertificateErrors": null,
"unstable": false
@@ -63,6 +69,9 @@
"workspace": {
"configuration": true,
"workspaceFolders": true
+ },
+ "experimental": {
+ "testingApi": true
}
}
}
diff --git a/cli/tools/test.rs b/cli/tools/test.rs
index e29d9a220..242abab1b 100644
--- a/cli/tools/test.rs
+++ b/cli/tools/test.rs
@@ -58,7 +58,7 @@ use tokio::sync::mpsc::UnboundedSender;
/// The test mode is used to determine how a specifier is to be tested.
#[derive(Debug, Clone, PartialEq)]
-enum TestMode {
+pub enum TestMode {
/// Test as documentation, type-checking fenced code blocks.
Documentation,
/// Test as an executable module, loading the module into the isolate and running each test it
@@ -163,7 +163,7 @@ struct TestSpecifierOptions {
}
impl TestSummary {
- fn new() -> TestSummary {
+ pub fn new() -> TestSummary {
TestSummary {
total: 0,
passed: 0,
@@ -188,7 +188,7 @@ impl TestSummary {
}
}
-trait TestReporter {
+pub trait TestReporter {
fn report_plan(&mut self, plan: &TestPlan);
fn report_wait(&mut self, description: &TestDescription);
fn report_output(&mut self, output: &TestOutput);
@@ -718,7 +718,7 @@ async fn fetch_inline_files(
}
/// Type check a collection of module and document specifiers.
-async fn check_specifiers(
+pub async fn check_specifiers(
ps: &ProcState,
permissions: Permissions,
specifiers: Vec<(ModuleSpecifier, TestMode)>,
diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js
index 3e4a57df1..abbef2ae4 100644
--- a/runtime/js/40_testing.js
+++ b/runtime/js/40_testing.js
@@ -750,23 +750,37 @@
return inspectArgs([error]);
}
+ /**
+ * @param {string | { include?: string[], exclude?: string[] }} filter
+ * @returns {(def: { name: string }) => boolean}
+ */
function createTestFilter(filter) {
+ if (!filter) {
+ return () => true;
+ }
+
+ const regex =
+ typeof filter === "string" && StringPrototypeStartsWith(filter, "/") &&
+ StringPrototypeEndsWith(filter, "/")
+ ? new RegExp(StringPrototypeSlice(filter, 1, filter.length - 1))
+ : undefined;
+
+ const filterIsObject = filter != null && typeof filter === "object";
+
return (def) => {
- if (filter) {
- if (
- StringPrototypeStartsWith(filter, "/") &&
- StringPrototypeEndsWith(filter, "/")
- ) {
- const regex = new RegExp(
- StringPrototypeSlice(filter, 1, filter.length - 1),
- );
- return RegExpPrototypeTest(regex, def.name);
+ if (regex) {
+ return RegExpPrototypeTest(regex, def.name);
+ }
+ if (filterIsObject) {
+ if (filter.include && !filter.include.includes(def.name)) {
+ return false;
+ } else if (filter.exclude && filter.exclude.includes(def.name)) {
+ return false;
+ } else {
+ return true;
}
-
- return StringPrototypeIncludes(def.name, filter);
}
-
- return true;
+ return StringPrototypeIncludes(def.name, filter);
};
}
diff --git a/test_util/src/lsp.rs b/test_util/src/lsp.rs
index 948dc4da6..9d5a74eaf 100644
--- a/test_util/src/lsp.rs
+++ b/test_util/src/lsp.rs
@@ -167,15 +167,18 @@ where
}
impl LspClient {
- pub fn new(deno_exe: &Path) -> Result<Self> {
+ pub fn new(deno_exe: &Path, print_stderr: bool) -> Result<Self> {
let deno_dir = new_deno_dir();
- let mut child = Command::new(deno_exe)
+ let mut command = Command::new(deno_exe);
+ command
.env("DENO_DIR", deno_dir.path())
.arg("lsp")
.stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::null())
- .spawn()?;
+ .stdout(Stdio::piped());
+ if !print_stderr {
+ command.stderr(Stdio::null());
+ }
+ let mut child = command.spawn()?;
let stdout = child.stdout.take().unwrap();
let reader = io::BufReader::new(stdout);