mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 12:19:12 +00: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
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"],
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue