mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
feat(lsp): add experimental testing API (#13798)
Ref: denoland/vscode_deno#629
This commit is contained in:
parent
4a0b2c28a1
commit
061090de7e
20 changed files with 2608 additions and 52 deletions
|
@ -44,7 +44,7 @@ struct FixtureMessage {
|
||||||
/// the end of the document and does a level of hovering and gets quick fix
|
/// the end of the document and does a level of hovering and gets quick fix
|
||||||
/// code actions.
|
/// code actions.
|
||||||
fn bench_big_file_edits(deno_exe: &Path) -> Result<Duration, AnyError> {
|
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 params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
|
||||||
let (_, response_error): (Option<Value>, Option<LspResponseError>) =
|
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> {
|
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 params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
|
||||||
let (_, maybe_err) =
|
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> {
|
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 params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
|
||||||
let (_, maybe_err) =
|
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.
|
/// A test that starts up the LSP, opens a single line document, and exits.
|
||||||
fn bench_startup_shutdown(deno_exe: &Path) -> Result<Duration, AnyError> {
|
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 params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
|
||||||
let (_, response_error) =
|
let (_, response_error) =
|
||||||
|
|
|
@ -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
|
// 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) {
|
fn incremental_change_wait(bench: &mut Bencher) {
|
||||||
let deno_exe = test_util::deno_exe_path();
|
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] =
|
static FIXTURE_INIT_JSON: &[u8] =
|
||||||
include_bytes!("testdata/initialize_params.json");
|
include_bytes!("testdata/initialize_params.json");
|
||||||
|
|
|
@ -472,7 +472,11 @@ To evaluate code in the shell:
|
||||||
";
|
";
|
||||||
|
|
||||||
/// Main entry point for parsing deno's command line flags.
|
/// 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 version = crate::version::deno();
|
||||||
let app = clap_root(&version);
|
let app = clap_root(&version);
|
||||||
let matches = app.clone().try_get_matches_from(args)?;
|
let matches = app.clone().try_get_matches_from(args)?;
|
||||||
|
|
|
@ -138,6 +138,7 @@ pub fn server_capabilities(
|
||||||
moniker_provider: None,
|
moniker_provider: None,
|
||||||
experimental: Some(json!({
|
experimental: Some(json!({
|
||||||
"denoConfigTasks": true,
|
"denoConfigTasks": true,
|
||||||
|
"testingApi":true,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,14 @@ use crate::lsp::repl::get_repl_workspace_settings;
|
||||||
use super::config::SpecifierSettings;
|
use super::config::SpecifierSettings;
|
||||||
use super::config::SETTINGS_SECTION;
|
use super::config::SETTINGS_SECTION;
|
||||||
use super::lsp_custom;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct Client(Arc<dyn ClientTrait>);
|
pub struct Client(Arc<dyn ClientTrait>);
|
||||||
|
@ -51,6 +59,10 @@ impl Client {
|
||||||
self.0.send_registry_state_notification(params).await;
|
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(
|
pub async fn specifier_configurations(
|
||||||
&self,
|
&self,
|
||||||
specifiers: Vec<lsp::Url>,
|
specifiers: Vec<lsp::Url>,
|
||||||
|
@ -118,6 +130,7 @@ trait ClientTrait: Send + Sync {
|
||||||
&self,
|
&self,
|
||||||
params: lsp_custom::RegistryStateNotificationParams,
|
params: lsp_custom::RegistryStateNotificationParams,
|
||||||
) -> AsyncReturn<()>;
|
) -> AsyncReturn<()>;
|
||||||
|
fn send_test_notification(&self, params: TestingNotification);
|
||||||
fn specifier_configurations(
|
fn specifier_configurations(
|
||||||
&self,
|
&self,
|
||||||
uris: Vec<lsp::Url>,
|
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(
|
fn specifier_configurations(
|
||||||
&self,
|
&self,
|
||||||
uris: Vec<lsp::Url>,
|
uris: Vec<lsp::Url>,
|
||||||
|
@ -260,6 +298,8 @@ impl ClientTrait for ReplClient {
|
||||||
Box::pin(future::ready(()))
|
Box::pin(future::ready(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_test_notification(&self, _params: TestingNotification) {}
|
||||||
|
|
||||||
fn specifier_configurations(
|
fn specifier_configurations(
|
||||||
&self,
|
&self,
|
||||||
uris: Vec<lsp::Url>,
|
uris: Vec<lsp::Url>,
|
||||||
|
|
|
@ -21,6 +21,10 @@ pub struct ClientCapabilities {
|
||||||
pub code_action_disabled_support: bool,
|
pub code_action_disabled_support: bool,
|
||||||
pub line_folding_only: bool,
|
pub line_folding_only: bool,
|
||||||
pub status_notification: 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_configuration: bool,
|
||||||
pub workspace_did_change_watched_files: bool,
|
pub workspace_did_change_watched_files: bool,
|
||||||
}
|
}
|
||||||
|
@ -139,6 +143,28 @@ pub struct SpecifierSettings {
|
||||||
pub code_lens: CodeLensSpecifierSettings,
|
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.
|
/// Deno language server specific settings that are applied to a workspace.
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -184,6 +210,10 @@ pub struct WorkspaceSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub suggest: CompletionSettings,
|
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
|
/// An option which sets the cert file to use when attempting to fetch remote
|
||||||
/// resources. This overrides `DENO_CERT` if present.
|
/// resources. This overrides `DENO_CERT` if present.
|
||||||
pub tls_certificate: Option<String>,
|
pub tls_certificate: Option<String>,
|
||||||
|
@ -333,7 +363,10 @@ impl Config {
|
||||||
self.client_capabilities.status_notification = experimental
|
self.client_capabilities.status_notification = experimental
|
||||||
.get("statusNotification")
|
.get("statusNotification")
|
||||||
.and_then(|it| it.as_bool())
|
.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 {
|
if let Some(workspace) = &capabilities.workspace {
|
||||||
|
@ -530,6 +563,10 @@ mod tests {
|
||||||
hosts: HashMap::new(),
|
hosts: HashMap::new(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
testing: TestingSettings {
|
||||||
|
args: vec!["--allow-all".to_string(), "--no-check".to_string()],
|
||||||
|
enable: true
|
||||||
|
},
|
||||||
tls_certificate: None,
|
tls_certificate: None,
|
||||||
unsafely_ignore_certificate_errors: None,
|
unsafely_ignore_certificate_errors: None,
|
||||||
unstable: false,
|
unstable: false,
|
||||||
|
|
|
@ -47,6 +47,7 @@ use super::performance::Performance;
|
||||||
use super::refactor;
|
use super::refactor;
|
||||||
use super::registries::ModuleRegistry;
|
use super::registries::ModuleRegistry;
|
||||||
use super::registries::ModuleRegistryOptions;
|
use super::registries::ModuleRegistryOptions;
|
||||||
|
use super::testing;
|
||||||
use super::text;
|
use super::text;
|
||||||
use super::tsc;
|
use super::tsc;
|
||||||
use super::tsc::Assets;
|
use super::tsc::Assets;
|
||||||
|
@ -107,14 +108,16 @@ pub struct Inner {
|
||||||
/// An optional configuration file which has been specified in the client
|
/// An optional configuration file which has been specified in the client
|
||||||
/// options.
|
/// options.
|
||||||
maybe_config_file: Option<ConfigFile>,
|
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.
|
/// An optional configuration for formatter which has been taken from specified config file.
|
||||||
maybe_fmt_config: Option<FmtConfig>,
|
maybe_fmt_config: Option<FmtConfig>,
|
||||||
/// An optional import map which is used to resolve modules.
|
/// An optional import map which is used to resolve modules.
|
||||||
pub maybe_import_map: Option<Arc<ImportMap>>,
|
pub maybe_import_map: Option<Arc<ImportMap>>,
|
||||||
/// The URL for the import map which is used to determine relative imports.
|
/// The URL for the import map which is used to determine relative imports.
|
||||||
maybe_import_map_uri: Option<Url>,
|
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.
|
/// A collection of measurements which instrument that performance of the LSP.
|
||||||
performance: Arc<Performance>,
|
performance: Arc<Performance>,
|
||||||
/// A memoized version of fixable diagnostic codes retrieved from TypeScript.
|
/// A memoized version of fixable diagnostic codes retrieved from TypeScript.
|
||||||
|
@ -163,12 +166,13 @@ impl Inner {
|
||||||
diagnostics_server,
|
diagnostics_server,
|
||||||
documents,
|
documents,
|
||||||
maybe_cache_path: None,
|
maybe_cache_path: None,
|
||||||
maybe_lint_config: None,
|
|
||||||
maybe_fmt_config: None,
|
|
||||||
maybe_cache_server: None,
|
maybe_cache_server: None,
|
||||||
maybe_config_file: None,
|
maybe_config_file: None,
|
||||||
maybe_import_map: None,
|
maybe_import_map: None,
|
||||||
maybe_import_map_uri: None,
|
maybe_import_map_uri: None,
|
||||||
|
maybe_lint_config: None,
|
||||||
|
maybe_fmt_config: None,
|
||||||
|
maybe_testing_server: None,
|
||||||
module_registries,
|
module_registries,
|
||||||
module_registries_location,
|
module_registries_location,
|
||||||
performance,
|
performance,
|
||||||
|
@ -781,6 +785,15 @@ impl Inner {
|
||||||
}
|
}
|
||||||
self.config.update_enabled_paths(self.client.clone()).await;
|
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.");
|
lsp_log!("Server ready.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -835,6 +848,7 @@ impl Inner {
|
||||||
.diagnostics_server
|
.diagnostics_server
|
||||||
.invalidate(&self.documents.dependents(&specifier));
|
.invalidate(&self.documents.dependents(&specifier));
|
||||||
self.send_diagnostics_update();
|
self.send_diagnostics_update();
|
||||||
|
self.send_testing_update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => error!("{}", err),
|
Err(err) => error!("{}", err),
|
||||||
|
@ -860,6 +874,7 @@ impl Inner {
|
||||||
specifiers.push(specifier.clone());
|
specifiers.push(specifier.clone());
|
||||||
self.diagnostics_server.invalidate(&specifiers);
|
self.diagnostics_server.invalidate(&specifiers);
|
||||||
self.send_diagnostics_update();
|
self.send_diagnostics_update();
|
||||||
|
self.send_testing_update();
|
||||||
}
|
}
|
||||||
self.performance.measure(mark);
|
self.performance.measure(mark);
|
||||||
}
|
}
|
||||||
|
@ -909,6 +924,7 @@ impl Inner {
|
||||||
);
|
);
|
||||||
|
|
||||||
self.send_diagnostics_update();
|
self.send_diagnostics_update();
|
||||||
|
self.send_testing_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn did_change_watched_files(
|
async fn did_change_watched_files(
|
||||||
|
@ -954,6 +970,7 @@ impl Inner {
|
||||||
);
|
);
|
||||||
self.diagnostics_server.invalidate_all();
|
self.diagnostics_server.invalidate_all();
|
||||||
self.send_diagnostics_update();
|
self.send_diagnostics_update();
|
||||||
|
self.send_testing_update();
|
||||||
}
|
}
|
||||||
self.performance.measure(mark);
|
self.performance.measure(mark);
|
||||||
}
|
}
|
||||||
|
@ -2143,6 +2160,29 @@ impl Inner {
|
||||||
self.reload_import_registries().await
|
self.reload_import_registries().await
|
||||||
}
|
}
|
||||||
lsp_custom::TASK_REQUEST => self.get_tasks(),
|
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 => {
|
lsp_custom::VIRTUAL_TEXT_DOCUMENT => {
|
||||||
match params.map(serde_json::from_value) {
|
match params.map(serde_json::from_value) {
|
||||||
Some(Ok(params)) => Ok(Some(
|
Some(Ok(params)) => Ok(Some(
|
||||||
|
@ -2389,6 +2429,16 @@ impl Inner {
|
||||||
error!("Cannot update diagnostics: {}", err);
|
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]
|
#[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
|
// don't send diagnostics yet if we don't have the specifier settings
|
||||||
if has_specifier_settings {
|
if has_specifier_settings {
|
||||||
inner.send_diagnostics_update();
|
inner.send_diagnostics_update();
|
||||||
|
inner.send_testing_update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(client, uri, specifier, has_specifier_settings)
|
(client, uri, specifier, has_specifier_settings)
|
||||||
|
@ -2464,6 +2515,7 @@ impl lspower::LanguageServer for LanguageServer {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
inner.send_diagnostics_update();
|
inner.send_diagnostics_update();
|
||||||
|
inner.send_testing_update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2823,6 +2875,7 @@ impl Inner {
|
||||||
// invalidate some diagnostics
|
// invalidate some diagnostics
|
||||||
self.diagnostics_server.invalidate(&[referrer]);
|
self.diagnostics_server.invalidate(&[referrer]);
|
||||||
self.send_diagnostics_update();
|
self.send_diagnostics_update();
|
||||||
|
self.send_testing_update();
|
||||||
|
|
||||||
self.performance.measure(mark);
|
self.performance.measure(mark);
|
||||||
Ok(Some(json!(true)))
|
Ok(Some(json!(true)))
|
||||||
|
|
|
@ -26,6 +26,7 @@ mod refactor;
|
||||||
mod registries;
|
mod registries;
|
||||||
mod repl;
|
mod repl;
|
||||||
mod semantic_tokens;
|
mod semantic_tokens;
|
||||||
|
mod testing;
|
||||||
mod text;
|
mod text;
|
||||||
mod tsc;
|
mod tsc;
|
||||||
mod urls;
|
mod urls;
|
||||||
|
|
|
@ -36,6 +36,7 @@ use lspower::LanguageServer;
|
||||||
use super::client::Client;
|
use super::client::Client;
|
||||||
use super::config::CompletionSettings;
|
use super::config::CompletionSettings;
|
||||||
use super::config::ImportCompletionSettings;
|
use super::config::ImportCompletionSettings;
|
||||||
|
use super::config::TestingSettings;
|
||||||
use super::config::WorkspaceSettings;
|
use super::config::WorkspaceSettings;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -294,5 +295,9 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings {
|
||||||
hosts: HashMap::from([("https://deno.land".to_string(), true)]),
|
hosts: HashMap::from([("https://deno.land".to_string(), true)]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
testing: TestingSettings {
|
||||||
|
args: vec![],
|
||||||
|
enable: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
619
cli/lsp/testing/collectors.rs
Normal file
619
cli/lsp/testing/collectors.rs
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
180
cli/lsp/testing/definitions.rs
Normal file
180
cli/lsp/testing/definitions.rs
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
947
cli/lsp/testing/execution.rs
Normal file
947
cli/lsp/testing/execution.rs
Normal file
|
@ -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) = ¶ms.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) = ¶ms.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(¶ms, &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"],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
186
cli/lsp/testing/lsp_custom.rs
Normal file
186
cli/lsp/testing/lsp_custom.rs
Normal file
|
@ -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";
|
||||||
|
}
|
11
cli/lsp/testing/mod.rs
Normal file
11
cli/lsp/testing/mod.rs
Normal file
|
@ -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;
|
219
cli/lsp/testing/server.rs
Normal file
219
cli/lsp/testing/server.rs
Normal file
|
@ -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(¶ms.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(¶ms, 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ fn load_fixture_str(path: &str) -> String {
|
||||||
|
|
||||||
fn init(init_path: &str) -> LspClient {
|
fn init(init_path: &str) -> LspClient {
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", load_fixture(init_path))
|
.write_request::<_, _, Value>("initialize", load_fixture(init_path))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -247,7 +247,7 @@ fn lsp_init_tsconfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -290,7 +290,7 @@ fn lsp_tsconfig_types() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -350,7 +350,7 @@ fn lsp_triple_slash_types() {
|
||||||
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
|
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -397,7 +397,7 @@ fn lsp_import_map() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -516,7 +516,7 @@ fn lsp_import_map_config_file() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -599,7 +599,7 @@ fn lsp_deno_task() {
|
||||||
params.root_uri = Some(Url::from_file_path(workspace_root).unwrap());
|
params.root_uri = Some(Url::from_file_path(workspace_root).unwrap());
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -725,7 +725,7 @@ fn lsp_import_map_import_completions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1085,7 +1085,7 @@ fn lsp_workspace_enable_paths() {
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -2240,7 +2240,7 @@ fn lsp_format_exclude_with_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -2292,7 +2292,7 @@ fn lsp_format_exclude_default_config() {
|
||||||
params.root_uri = Some(Url::from_file_path(workspace_root.clone()).unwrap());
|
params.root_uri = Some(Url::from_file_path(workspace_root.clone()).unwrap());
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -3824,7 +3824,7 @@ fn lsp_cache_location() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -3945,7 +3945,7 @@ fn lsp_tls_cert() {
|
||||||
params.root_uri = Some(Url::from_file_path(testdata_path()).unwrap());
|
params.root_uri = Some(Url::from_file_path(testdata_path()).unwrap());
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -4426,7 +4426,7 @@ fn lsp_performance() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(maybe_err.is_none());
|
assert!(maybe_err.is_none());
|
||||||
if let Some(res) = maybe_res {
|
if let Some(res) = maybe_res {
|
||||||
assert_eq!(res.averages.len(), 13);
|
assert_eq!(res.averages.len(), 14);
|
||||||
} else {
|
} else {
|
||||||
panic!("unexpected result");
|
panic!("unexpected result");
|
||||||
}
|
}
|
||||||
|
@ -4622,7 +4622,7 @@ fn lsp_format_with_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -5101,7 +5101,7 @@ fn lsp_lint_with_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -5134,7 +5134,7 @@ fn lsp_lint_exclude_with_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deno_exe = deno_exe_path();
|
let deno_exe = deno_exe_path();
|
||||||
let mut client = LspClient::new(&deno_exe).unwrap();
|
let mut client = LspClient::new(&deno_exe, false).unwrap();
|
||||||
client
|
client
|
||||||
.write_request::<_, _, Value>("initialize", params)
|
.write_request::<_, _, Value>("initialize", params)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -5236,3 +5236,230 @@ export function B() {
|
||||||
);
|
);
|
||||||
shutdown(&mut client);
|
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 = ¶ms.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);
|
||||||
|
}
|
||||||
|
|
11
cli/tests/testdata/lsp/initialize_params.json
vendored
11
cli/tests/testdata/lsp/initialize_params.json
vendored
|
@ -14,7 +14,7 @@
|
||||||
"references": true,
|
"references": true,
|
||||||
"test": true
|
"test": true
|
||||||
},
|
},
|
||||||
"config": "",
|
"config": null,
|
||||||
"importMap": null,
|
"importMap": null,
|
||||||
"lint": true,
|
"lint": true,
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
@ -26,6 +26,12 @@
|
||||||
"hosts": {}
|
"hosts": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"testing": {
|
||||||
|
"args": [
|
||||||
|
"--allow-all"
|
||||||
|
],
|
||||||
|
"enable": true
|
||||||
|
},
|
||||||
"tlsCertificate": null,
|
"tlsCertificate": null,
|
||||||
"unsafelyIgnoreCertificateErrors": null,
|
"unsafelyIgnoreCertificateErrors": null,
|
||||||
"unstable": false
|
"unstable": false
|
||||||
|
@ -63,6 +69,9 @@
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"configuration": true,
|
"configuration": true,
|
||||||
"workspaceFolders": true
|
"workspaceFolders": true
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"testingApi": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
/// The test mode is used to determine how a specifier is to be tested.
|
/// The test mode is used to determine how a specifier is to be tested.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
enum TestMode {
|
pub enum TestMode {
|
||||||
/// Test as documentation, type-checking fenced code blocks.
|
/// Test as documentation, type-checking fenced code blocks.
|
||||||
Documentation,
|
Documentation,
|
||||||
/// Test as an executable module, loading the module into the isolate and running each test it
|
/// Test as an executable module, loading the module into the isolate and running each test it
|
||||||
|
@ -163,7 +163,7 @@ struct TestSpecifierOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestSummary {
|
impl TestSummary {
|
||||||
fn new() -> TestSummary {
|
pub fn new() -> TestSummary {
|
||||||
TestSummary {
|
TestSummary {
|
||||||
total: 0,
|
total: 0,
|
||||||
passed: 0,
|
passed: 0,
|
||||||
|
@ -188,7 +188,7 @@ impl TestSummary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait TestReporter {
|
pub trait TestReporter {
|
||||||
fn report_plan(&mut self, plan: &TestPlan);
|
fn report_plan(&mut self, plan: &TestPlan);
|
||||||
fn report_wait(&mut self, description: &TestDescription);
|
fn report_wait(&mut self, description: &TestDescription);
|
||||||
fn report_output(&mut self, output: &TestOutput);
|
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.
|
/// Type check a collection of module and document specifiers.
|
||||||
async fn check_specifiers(
|
pub async fn check_specifiers(
|
||||||
ps: &ProcState,
|
ps: &ProcState,
|
||||||
permissions: Permissions,
|
permissions: Permissions,
|
||||||
specifiers: Vec<(ModuleSpecifier, TestMode)>,
|
specifiers: Vec<(ModuleSpecifier, TestMode)>,
|
||||||
|
|
|
@ -750,23 +750,37 @@
|
||||||
return inspectArgs([error]);
|
return inspectArgs([error]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | { include?: string[], exclude?: string[] }} filter
|
||||||
|
* @returns {(def: { name: string }) => boolean}
|
||||||
|
*/
|
||||||
function createTestFilter(filter) {
|
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) => {
|
return (def) => {
|
||||||
if (filter) {
|
if (regex) {
|
||||||
if (
|
return RegExpPrototypeTest(regex, def.name);
|
||||||
StringPrototypeStartsWith(filter, "/") &&
|
|
||||||
StringPrototypeEndsWith(filter, "/")
|
|
||||||
) {
|
|
||||||
const regex = new RegExp(
|
|
||||||
StringPrototypeSlice(filter, 1, filter.length - 1),
|
|
||||||
);
|
|
||||||
return RegExpPrototypeTest(regex, def.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StringPrototypeIncludes(def.name, filter);
|
|
||||||
}
|
}
|
||||||
|
if (filterIsObject) {
|
||||||
return true;
|
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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,15 +167,18 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LspClient {
|
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 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())
|
.env("DENO_DIR", deno_dir.path())
|
||||||
.arg("lsp")
|
.arg("lsp")
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped());
|
||||||
.stderr(Stdio::null())
|
if !print_stderr {
|
||||||
.spawn()?;
|
command.stderr(Stdio::null());
|
||||||
|
}
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
|
||||||
let stdout = child.stdout.take().unwrap();
|
let stdout = child.stdout.take().unwrap();
|
||||||
let reader = io::BufReader::new(stdout);
|
let reader = io::BufReader::new(stdout);
|
||||||
|
|
Loading…
Reference in a new issue