Skip to content

Commit a8b7acf

Browse files
committed
Auto merge of #16971 - HKalbasi:test-explorer, r=HKalbasi
Resolve tests per file instead of per crate in test explorer Fix part of #16827
2 parents ab10eea + beec691 commit a8b7acf

File tree

8 files changed

+142
-40
lines changed

8 files changed

+142
-40
lines changed

crates/ide/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,10 @@ impl Analysis {
353353
self.with_db(|db| test_explorer::discover_tests_in_crate(db, crate_id))
354354
}
355355

356+
pub fn discover_tests_in_file(&self, file_id: FileId) -> Cancellable<Vec<TestItem>> {
357+
self.with_db(|db| test_explorer::discover_tests_in_file(db, file_id))
358+
}
359+
356360
/// Renders the crate graph to GraphViz "dot" syntax.
357361
pub fn view_crate_graph(&self, full: bool) -> Cancellable<Result<String, String>> {
358362
self.with_db(|db| view_crate_graph::view_crate_graph(db, full))

crates/ide/src/test_explorer.rs

+62-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use ide_db::{
77
};
88
use syntax::TextRange;
99

10-
use crate::{navigation_target::ToNav, runnables::runnable_fn, Runnable, TryToNav};
10+
use crate::{runnables::runnable_fn, NavigationTarget, Runnable, TryToNav};
1111

1212
#[derive(Debug)]
1313
pub enum TestItemKind {
@@ -56,17 +56,22 @@ fn find_crate_by_id(crate_graph: &CrateGraph, crate_id: &str) -> Option<CrateId>
5656
})
5757
}
5858

59-
fn discover_tests_in_module(db: &RootDatabase, module: Module, prefix_id: String) -> Vec<TestItem> {
59+
fn discover_tests_in_module(
60+
db: &RootDatabase,
61+
module: Module,
62+
prefix_id: String,
63+
only_in_this_file: bool,
64+
) -> Vec<TestItem> {
6065
let sema = Semantics::new(db);
6166

6267
let mut r = vec![];
6368
for c in module.children(db) {
6469
let module_name =
6570
c.name(db).as_ref().and_then(|n| n.as_str()).unwrap_or("[mod without name]").to_owned();
6671
let module_id = format!("{prefix_id}::{module_name}");
67-
let module_children = discover_tests_in_module(db, c, module_id.clone());
72+
let module_children = discover_tests_in_module(db, c, module_id.clone(), only_in_this_file);
6873
if !module_children.is_empty() {
69-
let nav = c.to_nav(db).call_site;
74+
let nav = NavigationTarget::from_module_to_decl(sema.db, c).call_site;
7075
r.push(TestItem {
7176
id: module_id,
7277
kind: TestItemKind::Module,
@@ -76,7 +81,9 @@ fn discover_tests_in_module(db: &RootDatabase, module: Module, prefix_id: String
7681
text_range: Some(nav.focus_or_full_range()),
7782
runnable: None,
7883
});
79-
r.extend(module_children);
84+
if !only_in_this_file || c.is_inline(db) {
85+
r.extend(module_children);
86+
}
8087
}
8188
}
8289
for def in module.declarations(db) {
@@ -112,6 +119,55 @@ pub(crate) fn discover_tests_in_crate_by_test_id(
112119
discover_tests_in_crate(db, crate_id)
113120
}
114121

122+
pub(crate) fn discover_tests_in_file(db: &RootDatabase, file_id: FileId) -> Vec<TestItem> {
123+
let sema = Semantics::new(db);
124+
125+
let Some(module) = sema.file_to_module_def(file_id) else { return vec![] };
126+
let Some((mut tests, id)) = find_module_id_and_test_parents(&sema, module) else {
127+
return vec![];
128+
};
129+
tests.extend(discover_tests_in_module(db, module, id, true));
130+
tests
131+
}
132+
133+
fn find_module_id_and_test_parents(
134+
sema: &Semantics<'_, RootDatabase>,
135+
module: Module,
136+
) -> Option<(Vec<TestItem>, String)> {
137+
let Some(parent) = module.parent(sema.db) else {
138+
let name = module.krate().display_name(sema.db)?.to_string();
139+
return Some((
140+
vec![TestItem {
141+
id: name.clone(),
142+
kind: TestItemKind::Crate(module.krate().into()),
143+
label: name.clone(),
144+
parent: None,
145+
file: None,
146+
text_range: None,
147+
runnable: None,
148+
}],
149+
name,
150+
));
151+
};
152+
let (mut r, mut id) = find_module_id_and_test_parents(sema, parent)?;
153+
let parent = Some(id.clone());
154+
id += "::";
155+
let module_name = &module.name(sema.db);
156+
let module_name = module_name.as_ref().and_then(|n| n.as_str()).unwrap_or("[mod without name]");
157+
id += module_name;
158+
let nav = NavigationTarget::from_module_to_decl(sema.db, module).call_site;
159+
r.push(TestItem {
160+
id: id.clone(),
161+
kind: TestItemKind::Module,
162+
label: module_name.to_owned(),
163+
parent,
164+
file: Some(nav.file_id),
165+
text_range: Some(nav.focus_or_full_range()),
166+
runnable: None,
167+
});
168+
Some((r, id))
169+
}
170+
115171
pub(crate) fn discover_tests_in_crate(db: &RootDatabase, crate_id: CrateId) -> Vec<TestItem> {
116172
let crate_graph = db.crate_graph();
117173
if !crate_graph[crate_id].origin.is_local() {
@@ -133,6 +189,6 @@ pub(crate) fn discover_tests_in_crate(db: &RootDatabase, crate_id: CrateId) -> V
133189
text_range: None,
134190
runnable: None,
135191
}];
136-
r.extend(discover_tests_in_module(db, module, crate_test_id));
192+
r.extend(discover_tests_in_module(db, module, crate_test_id, false));
137193
r
138194
}

crates/rust-analyzer/src/handlers/request.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,12 @@ pub(crate) fn handle_discover_test(
238238
let (tests, scope) = match params.test_id {
239239
Some(id) => {
240240
let crate_id = id.split_once("::").map(|it| it.0).unwrap_or(&id);
241-
(snap.analysis.discover_tests_in_crate_by_test_id(crate_id)?, vec![crate_id.to_owned()])
241+
(
242+
snap.analysis.discover_tests_in_crate_by_test_id(crate_id)?,
243+
Some(vec![crate_id.to_owned()]),
244+
)
242245
}
243-
None => (snap.analysis.discover_test_roots()?, vec![]),
246+
None => (snap.analysis.discover_test_roots()?, None),
244247
};
245248
for t in &tests {
246249
hack_recover_crate_name::insert_name(t.id.clone());
@@ -254,6 +257,7 @@ pub(crate) fn handle_discover_test(
254257
})
255258
.collect(),
256259
scope,
260+
scope_file: None,
257261
})
258262
}
259263

crates/rust-analyzer/src/lsp/ext.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ pub struct TestItem {
194194
#[serde(rename_all = "camelCase")]
195195
pub struct DiscoverTestResults {
196196
pub tests: Vec<TestItem>,
197-
pub scope: Vec<String>,
197+
pub scope: Option<Vec<String>>,
198+
pub scope_file: Option<Vec<TextDocumentIdentifier>>,
198199
}
199200

200201
pub enum DiscoverTest {}

crates/rust-analyzer/src/main_loop.rs

+11-14
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ use std::{
99
use always_assert::always;
1010
use crossbeam_channel::{never, select, Receiver};
1111
use ide_db::base_db::{SourceDatabase, SourceDatabaseExt, VfsPath};
12-
use itertools::Itertools;
1312
use lsp_server::{Connection, Notification, Request};
14-
use lsp_types::notification::Notification as _;
13+
use lsp_types::{notification::Notification as _, TextDocumentIdentifier};
1514
use stdx::thread::ThreadIntent;
1615
use vfs::FileId;
1716

@@ -533,22 +532,14 @@ impl GlobalState {
533532
let snapshot = self.snapshot();
534533
move || {
535534
let tests = subscriptions
536-
.into_iter()
537-
.filter_map(|f| snapshot.analysis.crates_for(f).ok())
538-
.flatten()
539-
.unique()
540-
.filter_map(|c| snapshot.analysis.discover_tests_in_crate(c).ok())
535+
.iter()
536+
.copied()
537+
.filter_map(|f| snapshot.analysis.discover_tests_in_file(f).ok())
541538
.flatten()
542539
.collect::<Vec<_>>();
543540
for t in &tests {
544541
hack_recover_crate_name::insert_name(t.id.clone());
545542
}
546-
let scope = tests
547-
.iter()
548-
.filter_map(|t| Some(t.id.split_once("::")?.0))
549-
.unique()
550-
.map(|it| it.to_owned())
551-
.collect();
552543
Task::DiscoverTest(lsp_ext::DiscoverTestResults {
553544
tests: tests
554545
.into_iter()
@@ -557,7 +548,13 @@ impl GlobalState {
557548
to_proto::test_item(&snapshot, t, line_index.as_ref())
558549
})
559550
.collect(),
560-
scope,
551+
scope: None,
552+
scope_file: Some(
553+
subscriptions
554+
.into_iter()
555+
.map(|f| TextDocumentIdentifier { uri: to_proto::url(&snapshot, f) })
556+
.collect(),
557+
),
561558
})
562559
}
563560
});

docs/dev/lsp-extensions.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!---
2-
lsp/ext.rs hash: d5febcbf63650753
2+
lsp/ext.rs hash: 223f48a89a5126a0
33
44
If you need to change the above hash to make the test pass, please check if you
55
need to adjust this doc as well and ping this issue:
@@ -440,7 +440,11 @@ interface DiscoverTestResults {
440440
// For each test which its id is in this list, the response
441441
// contains all tests that are children of this test, and
442442
// client should remove old tests not included in the response.
443-
scope: string[];
443+
scope: string[] | undefined;
444+
// For each file which its uri is in this list, the response
445+
// contains all tests that are located in this file, and
446+
// client should remove old tests not included in the response.
447+
scopeFile: lc.TextDocumentIdentifier[] | undefined;
444448
}
445449
```
446450

editors/code/src/lsp_ext.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ export type TestItem = {
8383
range?: lc.Range | undefined;
8484
runnable?: Runnable | undefined;
8585
};
86-
export type DiscoverTestResults = { tests: TestItem[]; scope: string[] };
86+
export type DiscoverTestResults = {
87+
tests: TestItem[];
88+
scope: string[] | undefined;
89+
scopeFile: lc.TextDocumentIdentifier[] | undefined;
90+
};
8791
export type TestState =
8892
| { tag: "failed"; message: string }
8993
| { tag: "passed" }

editors/code/src/test_explorer.ts

+46-14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const prepareTestExplorer = (
1212
) => {
1313
let currentTestRun: vscode.TestRun | undefined;
1414
let idToTestMap: Map<string, vscode.TestItem> = new Map();
15+
const fileToTestMap: Map<string, vscode.TestItem[]> = new Map();
1516
const idToRunnableMap: Map<string, ra.Runnable> = new Map();
1617

1718
testController.createRunProfile(
@@ -59,6 +60,18 @@ export const prepareTestExplorer = (
5960
false,
6061
);
6162

63+
const deleteTest = (item: vscode.TestItem, parentList: vscode.TestItemCollection) => {
64+
parentList.delete(item.id);
65+
idToTestMap.delete(item.id);
66+
idToRunnableMap.delete(item.id);
67+
if (item.uri) {
68+
fileToTestMap.set(
69+
item.uri.toString(),
70+
fileToTestMap.get(item.uri.toString())!.filter((t) => t.id !== item.id),
71+
);
72+
}
73+
};
74+
6275
const addTest = (item: ra.TestItem) => {
6376
const parentList = item.parent
6477
? idToTestMap.get(item.parent)!.children
@@ -76,7 +89,7 @@ export const prepareTestExplorer = (
7689
oldTest.range = range;
7790
return;
7891
}
79-
parentList.delete(item.id);
92+
deleteTest(oldTest, parentList);
8093
}
8194
const iconToVscodeMap = {
8295
package: "package",
@@ -91,14 +104,20 @@ export const prepareTestExplorer = (
91104
test.range = range;
92105
test.canResolveChildren = item.canResolveChildren;
93106
idToTestMap.set(item.id, test);
107+
if (uri) {
108+
if (!fileToTestMap.has(uri.toString())) {
109+
fileToTestMap.set(uri.toString(), []);
110+
}
111+
fileToTestMap.get(uri.toString())!.push(test);
112+
}
94113
if (item.runnable) {
95114
idToRunnableMap.set(item.id, item.runnable);
96115
}
97116
parentList.add(test);
98117
};
99118

100119
const addTestGroup = (testsAndScope: ra.DiscoverTestResults) => {
101-
const { tests, scope } = testsAndScope;
120+
const { tests, scope, scopeFile } = testsAndScope;
102121
const testSet: Set<string> = new Set();
103122
for (const test of tests) {
104123
addTest(test);
@@ -107,25 +126,38 @@ export const prepareTestExplorer = (
107126
// FIXME(hack_recover_crate_name): We eagerly resolve every test if we got a lazy top level response (detected
108127
// by checking that `scope` is empty). This is not a good thing and wastes cpu and memory unnecessarily, so we
109128
// should remove it.
110-
if (scope.length === 0) {
129+
if (!scope && !scopeFile) {
111130
for (const test of tests) {
112131
void testController.resolveHandler!(idToTestMap.get(test.id));
113132
}
114133
}
115-
if (!scope) {
116-
return;
134+
if (scope) {
135+
const recursivelyRemove = (tests: vscode.TestItemCollection) => {
136+
for (const [_, test] of tests) {
137+
if (!testSet.has(test.id)) {
138+
deleteTest(test, tests);
139+
} else {
140+
recursivelyRemove(test.children);
141+
}
142+
}
143+
};
144+
for (const root of scope) {
145+
recursivelyRemove(idToTestMap.get(root)!.children);
146+
}
117147
}
118-
const recursivelyRemove = (tests: vscode.TestItemCollection) => {
119-
for (const [testId, _] of tests) {
120-
if (!testSet.has(testId)) {
121-
tests.delete(testId);
122-
} else {
123-
recursivelyRemove(tests.get(testId)!.children);
148+
if (scopeFile) {
149+
const removeByFile = (file: vscode.Uri) => {
150+
const testsToBeRemoved = (fileToTestMap.get(file.toString()) || []).filter(
151+
(t) => !testSet.has(t.id),
152+
);
153+
for (const test of testsToBeRemoved) {
154+
const parentList = test.parent?.children || testController.items;
155+
deleteTest(test, parentList);
124156
}
157+
};
158+
for (const file of scopeFile) {
159+
removeByFile(vscode.Uri.parse(file.uri));
125160
}
126-
};
127-
for (const root of scope) {
128-
recursivelyRemove(idToTestMap.get(root)!.children);
129161
}
130162
};
131163

0 commit comments

Comments
 (0)