blob: 2e741021ea8bbf8830dea67911788d3453b6777d [file] [log] [blame]
//! Discovers tests
use hir::{Crate, Module, ModuleDef, Semantics};
use ide_db::{
base_db::{CrateGraph, CrateId, FileId, SourceDatabase},
RootDatabase,
};
use syntax::TextRange;
use crate::{navigation_target::ToNav, runnables::runnable_fn, Runnable, TryToNav};
#[derive(Debug)]
pub enum TestItemKind {
Crate,
Module,
Function,
}
#[derive(Debug)]
pub struct TestItem {
pub id: String,
pub kind: TestItemKind,
pub label: String,
pub parent: Option<String>,
pub file: Option<FileId>,
pub text_range: Option<TextRange>,
pub runnable: Option<Runnable>,
}
pub(crate) fn discover_test_roots(db: &RootDatabase) -> Vec<TestItem> {
let crate_graph = db.crate_graph();
crate_graph
.iter()
.filter(|&id| crate_graph[id].origin.is_local())
.filter_map(|id| Some(crate_graph[id].display_name.as_ref()?.to_string()))
.map(|id| TestItem {
kind: TestItemKind::Crate,
label: id.clone(),
id,
parent: None,
file: None,
text_range: None,
runnable: None,
})
.collect()
}
fn find_crate_by_id(crate_graph: &CrateGraph, crate_id: &str) -> Option<CrateId> {
// here, we use display_name as the crate id. This is not super ideal, but it works since we
// only show tests for the local crates.
crate_graph.iter().find(|&id| {
crate_graph[id].origin.is_local()
&& crate_graph[id].display_name.as_ref().is_some_and(|x| x.to_string() == crate_id)
})
}
fn discover_tests_in_module(db: &RootDatabase, module: Module, prefix_id: String) -> Vec<TestItem> {
let sema = Semantics::new(db);
let mut r = vec![];
for c in module.children(db) {
let module_name =
c.name(db).as_ref().and_then(|n| n.as_str()).unwrap_or("[mod without name]").to_owned();
let module_id = format!("{prefix_id}::{module_name}");
let module_children = discover_tests_in_module(db, c, module_id.clone());
if !module_children.is_empty() {
let nav = c.to_nav(db).call_site;
r.push(TestItem {
id: module_id,
kind: TestItemKind::Module,
label: module_name,
parent: Some(prefix_id.clone()),
file: Some(nav.file_id),
text_range: Some(nav.focus_or_full_range()),
runnable: None,
});
r.extend(module_children);
}
}
for def in module.declarations(db) {
let ModuleDef::Function(f) = def else {
continue;
};
if !f.is_test(db) {
continue;
}
let nav = f.try_to_nav(db).map(|r| r.call_site);
let fn_name = f.name(db).as_str().unwrap_or("[function without name]").to_owned();
r.push(TestItem {
id: format!("{prefix_id}::{fn_name}"),
kind: TestItemKind::Function,
label: fn_name,
parent: Some(prefix_id.clone()),
file: nav.as_ref().map(|n| n.file_id),
text_range: nav.as_ref().map(|n| n.focus_or_full_range()),
runnable: runnable_fn(&sema, f),
});
}
r
}
pub(crate) fn discover_tests_in_crate_by_test_id(
db: &RootDatabase,
crate_test_id: &str,
) -> Vec<TestItem> {
let crate_graph = db.crate_graph();
let Some(crate_id) = find_crate_by_id(&crate_graph, crate_test_id) else {
return vec![];
};
discover_tests_in_crate(db, crate_id)
}
pub(crate) fn discover_tests_in_crate(db: &RootDatabase, crate_id: CrateId) -> Vec<TestItem> {
let crate_graph = db.crate_graph();
if !crate_graph[crate_id].origin.is_local() {
return vec![];
}
let Some(crate_test_id) = &crate_graph[crate_id].display_name else {
return vec![];
};
let crate_test_id = crate_test_id.to_string();
let crate_id: Crate = crate_id.into();
let module = crate_id.root_module();
let mut r = vec![TestItem {
id: crate_test_id.clone(),
kind: TestItemKind::Crate,
label: crate_test_id.clone(),
parent: None,
file: None,
text_range: None,
runnable: None,
}];
r.extend(discover_tests_in_module(db, module, crate_test_id));
r
}