Consider exported_name="main" functions in test modules as tests

This commit is contained in:
Lukas Wirth 2024-04-04 14:51:10 +02:00
parent 2b62d4b2ba
commit 5957835cdf
6 changed files with 94 additions and 22 deletions

View file

@ -141,6 +141,10 @@ impl Attrs {
}
}
pub fn cfgs(&self) -> impl Iterator<Item = CfgExpr> + '_ {
self.by_key("cfg").tt_values().map(CfgExpr::parse)
}
pub(crate) fn is_cfg_enabled(&self, cfg_options: &CfgOptions) -> bool {
match self.cfg() {
None => true,

View file

@ -2006,12 +2006,15 @@ impl Function {
/// is this a `fn main` or a function with an `export_name` of `main`?
pub fn is_main(self, db: &dyn HirDatabase) -> bool {
if !self.module(db).is_crate_root() {
return false;
}
let data = db.function_data(self.id);
data.attrs.export_name() == Some("main")
|| self.module(db).is_crate_root() && data.name.to_smol_str() == "main"
}
data.name.to_smol_str() == "main" || data.attrs.export_name() == Some("main")
/// Is this a function with an `export_name` of `main`?
pub fn exported_main(self, db: &dyn HirDatabase) -> bool {
let data = db.function_data(self.id);
data.attrs.export_name() == Some("main")
}
/// Does this function have the ignore attribute?

View file

@ -3,7 +3,7 @@ use syntax::{
AstNode, AstToken,
};
use crate::{utils::test_related_attribute, AssistContext, AssistId, AssistKind, Assists};
use crate::{utils::test_related_attribute_syn, AssistContext, AssistId, AssistKind, Assists};
// Assist: toggle_ignore
//
@ -26,7 +26,7 @@ use crate::{utils::test_related_attribute, AssistContext, AssistId, AssistKind,
pub(crate) fn toggle_ignore(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let attr: ast::Attr = ctx.find_node_at_offset()?;
let func = attr.syntax().parent().and_then(ast::Fn::cast)?;
let attr = test_related_attribute(&func)?;
let attr = test_related_attribute_syn(&func)?;
match has_ignore_attribute(&func) {
None => acc.add(

View file

@ -71,7 +71,7 @@ pub fn extract_trivial_expression(block_expr: &ast::BlockExpr) -> Option<ast::Ex
///
/// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test,
/// but it's better than not to have the runnables for the tests at all.
pub fn test_related_attribute(fn_def: &ast::Fn) -> Option<ast::Attr> {
pub fn test_related_attribute_syn(fn_def: &ast::Fn) -> Option<ast::Attr> {
fn_def.attrs().find_map(|attr| {
let path = attr.path()?;
let text = path.syntax().text().to_string();
@ -83,6 +83,19 @@ pub fn test_related_attribute(fn_def: &ast::Fn) -> Option<ast::Attr> {
})
}
pub fn has_test_related_attribute(attrs: &hir::AttrsWithOwner) -> bool {
attrs.iter().any(|attr| {
let path = attr.path();
(|| {
Some(
path.segments().first()?.as_text()?.starts_with("test")
|| path.segments().last()?.as_text()?.ends_with("test"),
)
})()
.unwrap_or_default()
})
}
#[derive(Clone, Copy, PartialEq)]
pub enum IgnoreAssocItems {
DocHiddenAttrPresent,

View file

@ -2,7 +2,7 @@
//! We have to skip tests, so cannot reuse file_structure module.
use hir::Semantics;
use ide_assists::utils::test_related_attribute;
use ide_assists::utils::test_related_attribute_syn;
use ide_db::RootDatabase;
use syntax::{ast, ast::HasName, AstNode, SyntaxNode, TextRange};
@ -19,7 +19,7 @@ pub(super) fn find_all_methods(
fn method_range(item: SyntaxNode) -> Option<(TextRange, Option<TextRange>)> {
ast::Fn::cast(item).and_then(|fn_def| {
if test_related_attribute(&fn_def).is_some() {
if test_related_attribute_syn(&fn_def).is_some() {
None
} else {
Some((

View file

@ -1,9 +1,11 @@
use std::fmt;
use ast::HasName;
use cfg::CfgExpr;
use hir::{db::HirDatabase, AsAssocItem, HasAttrs, HasSource, HirFileIdExt, Semantics};
use ide_assists::utils::test_related_attribute;
use cfg::{CfgAtom, CfgExpr};
use hir::{
db::HirDatabase, AsAssocItem, AttrsWithOwner, HasAttrs, HasSource, HirFileIdExt, Semantics,
};
use ide_assists::utils::{has_test_related_attribute, test_related_attribute_syn};
use ide_db::{
base_db::{FilePosition, FileRange},
defs::Definition,
@ -280,7 +282,7 @@ fn find_related_tests_in_module(
}
fn as_test_runnable(sema: &Semantics<'_, RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> {
if test_related_attribute(fn_def).is_some() {
if test_related_attribute_syn(fn_def).is_some() {
let function = sema.to_def(fn_def)?;
runnable_fn(sema, function)
} else {
@ -293,7 +295,7 @@ fn parent_test_module(sema: &Semantics<'_, RootDatabase>, fn_def: &ast::Fn) -> O
let module = ast::Module::cast(node)?;
let module = sema.to_def(&module)?;
if has_test_function_or_multiple_test_submodules(sema, &module) {
if has_test_function_or_multiple_test_submodules(sema, &module, false) {
Some(module)
} else {
None
@ -305,7 +307,8 @@ pub(crate) fn runnable_fn(
sema: &Semantics<'_, RootDatabase>,
def: hir::Function,
) -> Option<Runnable> {
let kind = if def.is_main(sema.db) {
let under_cfg_test = has_cfg_test(def.module(sema.db).attrs(sema.db));
let kind = if !under_cfg_test && def.is_main(sema.db) {
RunnableKind::Bin
} else {
let test_id = || {
@ -342,7 +345,8 @@ pub(crate) fn runnable_mod(
sema: &Semantics<'_, RootDatabase>,
def: hir::Module,
) -> Option<Runnable> {
if !has_test_function_or_multiple_test_submodules(sema, &def) {
if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db)))
{
return None;
}
let path = def
@ -384,12 +388,17 @@ pub(crate) fn runnable_impl(
Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::DocTest { test_id }, cfg })
}
fn has_cfg_test(attrs: AttrsWithOwner) -> bool {
attrs.cfgs().any(|cfg| matches!(cfg, CfgExpr::Atom(CfgAtom::Flag(s)) if s == "test"))
}
/// Creates a test mod runnable for outline modules at the top of their definition.
fn runnable_mod_outline_definition(
sema: &Semantics<'_, RootDatabase>,
def: hir::Module,
) -> Option<Runnable> {
if !has_test_function_or_multiple_test_submodules(sema, &def) {
if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db)))
{
return None;
}
let path = def
@ -522,20 +531,28 @@ fn has_runnable_doc_test(attrs: &hir::Attrs) -> bool {
fn has_test_function_or_multiple_test_submodules(
sema: &Semantics<'_, RootDatabase>,
module: &hir::Module,
consider_exported_main: bool,
) -> bool {
let mut number_of_test_submodules = 0;
for item in module.declarations(sema.db) {
match item {
hir::ModuleDef::Function(f) => {
if let Some(it) = f.source(sema.db) {
if test_related_attribute(&it.value).is_some() {
return true;
}
if has_test_related_attribute(&f.attrs(sema.db)) {
return true;
}
if consider_exported_main && f.exported_main(sema.db) {
// an exported main in a test module can be considered a test wrt to custom test
// runners
return true;
}
}
hir::ModuleDef::Module(submodule) => {
if has_test_function_or_multiple_test_submodules(sema, &submodule) {
if has_test_function_or_multiple_test_submodules(
sema,
&submodule,
consider_exported_main,
) {
number_of_test_submodules += 1;
}
}
@ -1484,4 +1501,39 @@ mod r#mod {
"#]],
)
}
#[test]
fn exported_main_is_test_in_cfg_test_mod() {
check(
r#"
//- /lib.rs crate:foo cfg:test
$0
mod not_a_test_module_inline {
#[export_name = "main"]
fn exp_main() {}
}
#[cfg(test)]
mod test_mod_inline {
#[export_name = "main"]
fn exp_main() {}
}
mod not_a_test_module;
#[cfg(test)]
mod test_mod;
//- /not_a_test_module.rs
#[export_name = "main"]
fn exp_main() {}
//- /test_mod.rs
#[export_name = "main"]
fn exp_main() {}
"#,
expect![[r#"
[
"(Bin, NavigationTarget { file_id: FileId(0), full_range: 36..80, focus_range: 67..75, name: \"exp_main\", kind: Function })",
"(TestMod, NavigationTarget { file_id: FileId(0), full_range: 83..168, focus_range: 100..115, name: \"test_mod_inline\", kind: Module, description: \"mod test_mod_inline\" }, Atom(Flag(\"test\")))",
"(TestMod, NavigationTarget { file_id: FileId(0), full_range: 192..218, focus_range: 209..217, name: \"test_mod\", kind: Module, description: \"mod test_mod\" }, Atom(Flag(\"test\")))",
]
"#]],
)
}
}