1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-30 11:16:38 -05:00
denoland-deno/cli/lsp/testing/server.rs

239 lines
7.5 KiB
Rust
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
refactor(lsp): store test definitions in adjacency list (#20330) Previously: ```rust pub struct TestDefinition { pub id: String, pub name: String, pub range: SourceRange, pub steps: Vec<TestDefinition>, } pub struct TestDefinitions { pub discovered: Vec<TestDefinition>, pub injected: Vec<lsp_custom::TestData>, pub script_version: String, } ``` Now: ```rust pub struct TestDefinition { pub id: String, pub name: String, pub range: Option<Range>, pub is_dynamic: bool, // True for 'injected' module, not statically detected but added at runtime. pub parent_id: Option<String>, pub step_ids: HashSet<String>, } pub struct TestModule { pub specifier: ModuleSpecifier, pub script_version: String, pub defs: HashMap<String, TestDefinition>, } ``` Storing the test tree as a literal tree diminishes the value of IDs, even though vscode stores them that way. This makes all data easily accessible from `TestModule`. It unifies the interface between 'discovered' and 'injected' tests. This unblocks some enhancements wrt syncing tests between the LSP and extension, such as this TODO: https://github.com/denoland/vscode_deno/blob/61f08d5a71536a0a5f7dce965955b09e6bd957e1/client/src/testing.ts#L251-L259 and https://github.com/denoland/vscode_deno/issues/900. We should also get more flexibility overall. `TestCollector` is cleaned up, now stores a `&mut TestModule` directly and registers tests as it comes across them with `TestModule::register()`. This method ensures sanity in the redundant data from having both of `TestDefinition::{parent_id,step_ids}`. All of the messy conversions between `TestDescription`, `LspTestDescription`, `TestDefinition`, `TestData` and `TestIdentifier` are cleaned up. They shouldn't have been using `impl From` and now the full list of tests is available to their implementations.
2023-08-30 11:31:31 -04:00
use super::definitions::TestModule;
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::documents::DocumentsFilter;
use crate::lsp::language_server::StateSnapshot;
use crate::lsp::performance::Performance;
use crate::lsp::urls::url_to_uri;
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 std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::thread;
use tokio::sync::mpsc;
use tower_lsp::jsonrpc::Error as LspError;
use tower_lsp::jsonrpc::Result as LspResult;
use tower_lsp::lsp_types as lsp;
fn as_delete_notification(
url: &ModuleSpecifier,
) -> Result<TestingNotification, AnyError> {
Ok(TestingNotification::DeleteModule(
lsp_custom::TestModuleDeleteNotificationParams {
text_document: lsp::TextDocumentIdentifier {
uri: url_to_uri(url)?,
},
},
))
}
pub type TestServerTests =
Arc<tokio::sync::Mutex<HashMap<ModuleSpecifier, (TestModule, String)>>>;
/// 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: TestServerTests,
/// 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 = Default::default();
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("lsp.testing_update");
let mut tests = tests.lock().await;
// 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(DocumentsFilter::AllDiagnosable)
{
let specifier = document.specifier();
if specifier.scheme() != "file" {
continue;
}
if !snapshot.config.specifier_enabled_for_test(specifier) {
continue;
}
keys.remove(specifier);
let script_version = document.script_version();
let valid =
if let Some((_, old_script_version)) = tests.get(specifier) {
old_script_version == &script_version
} else {
false
};
if !valid {
let was_empty = tests
.remove(specifier)
.map(|(tm, _)| tm.is_empty())
.unwrap_or(true);
let test_module = document
.maybe_test_module()
.await
.map(|tm| tm.as_ref().clone())
.unwrap_or_else(|| TestModule::new(specifier.clone()));
if !test_module.is_empty() {
if let Ok(params) =
test_module.as_replace_notification(mru.as_ref())
{
client.send_test_notification(params);
}
} else if !was_empty {
if let Ok(params) = as_delete_notification(specifier) {
client.send_test_notification(params);
}
}
tests
.insert(specifier.clone(), (test_module, script_version));
}
}
for key in &keys {
if let Ok(params) = as_delete_notification(key) {
client.send_test_notification(params);
}
}
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);
}
}
client.send_test_notification(TestingNotification::Progress(
lsp_custom::TestRunProgressParams {
id,
message: lsp_custom::TestRunProgressMessage::End,
},
));
runs.lock().remove(&id);
}
}
}
}
})
});
server
}
fn enqueue_run(&self, id: u32) -> Result<(), AnyError> {
self.run_channel.send(id).map_err(|err| err.into())
}
/// A request from the client to cancel a test run.
pub fn run_cancel_request(
&self,
params: lsp_custom::TestRunCancelParams,
) -> LspResult<Option<Value>> {
if let Some(run) = self.runs.lock().get(&params.id) {
run.cancel();
Ok(Some(json!(true)))
} else {
Ok(Some(json!(false)))
}
}
/// A request from the client to start a test run.
pub async fn run_request(
&self,
params: lsp_custom::TestRunRequestParams,
workspace_settings: config::WorkspaceSettings,
) -> LspResult<Option<Value>> {
let test_run =
{ TestRun::init(&params, self.tests.clone(), workspace_settings).await };
let enqueued = test_run.as_enqueued().await;
{
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())
}
}