From 674cd5ab574dcc629e1cbf6b1e2713360b8617d8 Mon Sep 17 00:00:00 2001 From: hkalbasi Date: Thu, 22 Jun 2023 19:33:37 +0330 Subject: [PATCH] Add run-tests command --- crates/hir-def/src/attr.rs | 12 +++ crates/hir/src/lib.rs | 15 +++ crates/ide/src/runnables.rs | 21 ++--- crates/rust-analyzer/src/bin/main.rs | 1 + crates/rust-analyzer/src/cli.rs | 16 ++++ .../rust-analyzer/src/cli/analysis_stats.rs | 22 +---- crates/rust-analyzer/src/cli/flags.rs | 23 ++++- crates/rust-analyzer/src/cli/run_tests.rs | 92 +++++++++++++++++++ 8 files changed, 166 insertions(+), 36 deletions(-) create mode 100644 crates/rust-analyzer/src/cli/run_tests.rs diff --git a/crates/hir-def/src/attr.rs b/crates/hir-def/src/attr.rs index 09891f4452e..659ca2e9582 100644 --- a/crates/hir-def/src/attr.rs +++ b/crates/hir-def/src/attr.rs @@ -272,6 +272,18 @@ pub fn is_proc_macro_derive(&self) -> bool { self.by_key("proc_macro_derive").exists() } + pub fn is_test(&self) -> bool { + self.by_key("test").exists() + } + + pub fn is_ignore(&self) -> bool { + self.by_key("ignore").exists() + } + + pub fn is_bench(&self) -> bool { + self.by_key("bench").exists() + } + pub fn is_unstable(&self) -> bool { self.by_key("unstable").exists() } diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 1699c3dba77..352fa481509 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -1927,6 +1927,21 @@ pub fn is_async(self, db: &dyn HirDatabase) -> bool { db.function_data(self.id).has_async_kw() } + /// Does this function have `#[test]` attribute? + pub fn is_test(self, db: &dyn HirDatabase) -> bool { + db.function_data(self.id).attrs.is_test() + } + + /// Does this function have the ignore attribute? + pub fn is_ignore(self, db: &dyn HirDatabase) -> bool { + db.function_data(self.id).attrs.is_ignore() + } + + /// Does this function have `#[bench]` attribute? + pub fn is_bench(self, db: &dyn HirDatabase) -> bool { + db.function_data(self.id).attrs.is_bench() + } + pub fn is_unsafe_to_call(self, db: &dyn HirDatabase) -> bool { hir_ty::is_fn_unsafe_to_call(db, self.id) } diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index 27ad63d820d..1f331cd9323 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -2,7 +2,7 @@ use ast::HasName; use cfg::CfgExpr; -use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; +use hir::{db::HirDatabase, AsAssocItem, HasAttrs, HasSource, Semantics}; use ide_assists::utils::test_related_attribute; use ide_db::{ base_db::{FilePosition, FileRange}, @@ -14,7 +14,7 @@ use itertools::Itertools; use stdx::{always, format_to}; use syntax::{ - ast::{self, AstNode, HasAttrs as _}, + ast::{self, AstNode}, SmolStr, SyntaxNode, }; @@ -307,7 +307,6 @@ pub(crate) fn runnable_fn( sema: &Semantics<'_, RootDatabase>, def: hir::Function, ) -> Option { - let func = def.source(sema.db)?; let name = def.name(sema.db).to_smol_str(); let root = def.module(sema.db).krate().root_module(sema.db); @@ -323,10 +322,10 @@ pub(crate) fn runnable_fn( canonical_path.map(TestId::Path).unwrap_or(TestId::Name(name)) }; - if test_related_attribute(&func.value).is_some() { - let attr = TestAttr::from_fn(&func.value); + if def.is_test(sema.db) { + let attr = TestAttr::from_fn(sema.db, def); RunnableKind::Test { test_id: test_id(), attr } - } else if func.value.has_atom_attr("bench") { + } else if def.is_bench(sema.db) { RunnableKind::Bench { test_id: test_id() } } else { return None; @@ -335,7 +334,7 @@ pub(crate) fn runnable_fn( let nav = NavigationTarget::from_named( sema.db, - func.as_ref().map(|it| it as &dyn ast::HasName), + def.source(sema.db)?.as_ref().map(|it| it as &dyn ast::HasName), SymbolKind::Function, ); let cfg = def.attrs(sema.db).cfg(); @@ -487,12 +486,8 @@ pub struct TestAttr { } impl TestAttr { - fn from_fn(fn_def: &ast::Fn) -> TestAttr { - let ignore = fn_def - .attrs() - .filter_map(|attr| attr.simple_name()) - .any(|attribute_text| attribute_text == "ignore"); - TestAttr { ignore } + fn from_fn(db: &dyn HirDatabase, fn_def: hir::Function) -> TestAttr { + TestAttr { ignore: fn_def.is_ignore(db) } } } diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 118aad585ce..2fa14fc7e28 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -82,6 +82,7 @@ fn main() -> anyhow::Result<()> { flags::RustAnalyzerCmd::Search(cmd) => cmd.run()?, flags::RustAnalyzerCmd::Lsif(cmd) => cmd.run()?, flags::RustAnalyzerCmd::Scip(cmd) => cmd.run()?, + flags::RustAnalyzerCmd::RunTests(cmd) => cmd.run()?, } Ok(()) } diff --git a/crates/rust-analyzer/src/cli.rs b/crates/rust-analyzer/src/cli.rs index 34cd595634a..893eadf3fa1 100644 --- a/crates/rust-analyzer/src/cli.rs +++ b/crates/rust-analyzer/src/cli.rs @@ -10,12 +10,17 @@ mod ssr; mod lsif; mod scip; +mod run_tests; mod progress_report; use std::io::Read; +use anyhow::Result; +use hir::{Module, Name}; +use hir_ty::db::HirDatabase; use ide::AnalysisHost; +use itertools::Itertools; use vfs::Vfs; #[derive(Clone, Copy)] @@ -70,3 +75,14 @@ fn print_memory_usage(mut host: AnalysisHost, vfs: Vfs) { eprintln!("{remaining:>8} Remaining"); } + +fn full_name_of_item(db: &dyn HirDatabase, module: Module, name: Name) -> String { + module + .path_to_root(db) + .into_iter() + .rev() + .filter_map(|it| it.name(db)) + .chain(Some(name)) + .map(|it| it.display(db.upcast()).to_string()) + .join("::") +} diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs index e3b9f215bee..826b89926bf 100644 --- a/crates/rust-analyzer/src/cli/analysis_stats.rs +++ b/crates/rust-analyzer/src/cli/analysis_stats.rs @@ -34,6 +34,7 @@ use crate::cli::{ flags::{self, OutputFormat}, + full_name_of_item, load_cargo::{load_workspace, LoadCargoConfig, ProcMacroServerChoice}, print_memory_usage, progress_report::ProgressReport, @@ -274,15 +275,7 @@ fn run_data_layout(&self, db: &RootDatabase, adts: &[hir::Adt], verbosity: Verbo continue }; if verbosity.is_spammy() { - let full_name = a - .module(db) - .path_to_root(db) - .into_iter() - .rev() - .filter_map(|it| it.name(db)) - .chain(Some(a.name(db))) - .map(|it| it.display(db).to_string()) - .join("::"); + let full_name = full_name_of_item(db, a.module(db), a.name(db)); println!("Data layout for {full_name} failed due {e:?}"); } fail += 1; @@ -304,15 +297,8 @@ fn run_const_eval(&self, db: &RootDatabase, consts: &[hir::Const], verbosity: Ve continue; }; if verbosity.is_spammy() { - let full_name = c - .module(db) - .path_to_root(db) - .into_iter() - .rev() - .filter_map(|it| it.name(db)) - .chain(c.name(db)) - .map(|it| it.display(db).to_string()) - .join("::"); + let full_name = + full_name_of_item(db, c.module(db), c.name(db).unwrap_or(Name::missing())); println!("Const eval for {full_name} failed due {e:?}"); } fail += 1; diff --git a/crates/rust-analyzer/src/cli/flags.rs b/crates/rust-analyzer/src/cli/flags.rs index 208a4e6ecd9..9848739504a 100644 --- a/crates/rust-analyzer/src/cli/flags.rs +++ b/crates/rust-analyzer/src/cli/flags.rs @@ -90,6 +90,12 @@ optional --skip-const-eval } + /// Run unit tests of the project using mir interpreter + cmd run-tests { + /// Directory with Cargo.toml. + required path: PathBuf + } + cmd diagnostics { /// Directory with Cargo.toml. required path: PathBuf @@ -147,6 +153,7 @@ pub enum RustAnalyzerCmd { Symbols(Symbols), Highlight(Highlight), AnalysisStats(AnalysisStats), + RunTests(RunTests), Diagnostics(Diagnostics), Ssr(Ssr), Search(Search), @@ -182,16 +189,21 @@ pub struct AnalysisStats { pub parallel: bool, pub memory_usage: bool, pub source_stats: bool, - pub skip_lowering: bool, - pub skip_inference: bool, - pub skip_mir_stats: bool, - pub skip_data_layout: bool, - pub skip_const_eval: bool, pub only: Option, pub with_deps: bool, pub no_sysroot: bool, pub disable_build_scripts: bool, pub disable_proc_macros: bool, + pub skip_lowering: bool, + pub skip_inference: bool, + pub skip_mir_stats: bool, + pub skip_data_layout: bool, + pub skip_const_eval: bool, +} + +#[derive(Debug)] +pub struct RunTests { + pub path: PathBuf, } #[derive(Debug)] @@ -223,6 +235,7 @@ pub struct Lsif { #[derive(Debug)] pub struct Scip { pub path: PathBuf, + pub output: Option, } diff --git a/crates/rust-analyzer/src/cli/run_tests.rs b/crates/rust-analyzer/src/cli/run_tests.rs new file mode 100644 index 00000000000..bebbf26b54c --- /dev/null +++ b/crates/rust-analyzer/src/cli/run_tests.rs @@ -0,0 +1,92 @@ +//! Run all tests in a project, similar to `cargo test`, but using the mir interpreter. + +use hir::{Crate, Module}; +use hir_ty::db::HirDatabase; +use ide_db::{base_db::SourceDatabaseExt, LineIndexDatabase}; +use profile::StopWatch; +use project_model::{CargoConfig, RustLibSource}; +use syntax::TextRange; + +use crate::cli::{ + flags, full_name_of_item, + load_cargo::load_workspace_at, + load_cargo::{LoadCargoConfig, ProcMacroServerChoice}, + Result, +}; + +impl flags::RunTests { + pub fn run(self) -> Result<()> { + let mut cargo_config = CargoConfig::default(); + cargo_config.sysroot = Some(RustLibSource::Discover); + let load_cargo_config = LoadCargoConfig { + load_out_dirs_from_check: true, + with_proc_macro_server: ProcMacroServerChoice::Sysroot, + prefill_caches: false, + }; + let (host, _vfs, _proc_macro) = + load_workspace_at(&self.path, &cargo_config, &load_cargo_config, &|_| {})?; + let db = host.raw_database(); + + let tests = all_modules(db) + .into_iter() + .flat_map(|x| x.declarations(db)) + .filter_map(|x| match x { + hir::ModuleDef::Function(f) => Some(f), + _ => None, + }) + .filter(|x| x.is_test(db)); + let span_formatter = |file_id, text_range: TextRange| { + let line_col = match db.line_index(file_id).try_line_col(text_range.start()) { + None => " (unknown line col)".to_string(), + Some(x) => format!("#{}:{}", x.line + 1, x.col), + }; + let path = &db + .source_root(db.file_source_root(file_id)) + .path_for_file(&file_id) + .map(|x| x.to_string()); + let path = path.as_deref().unwrap_or(""); + format!("file://{path}{line_col}") + }; + let mut pass_count = 0; + let mut ignore_count = 0; + let mut fail_count = 0; + let mut sw_all = StopWatch::start(); + for test in tests { + let full_name = full_name_of_item(db, test.module(db), test.name(db)); + println!("test {}", full_name); + if test.is_ignore(db) { + println!("ignored"); + ignore_count += 1; + continue; + } + let mut sw_one = StopWatch::start(); + let result = test.eval(db, span_formatter); + if result.trim() == "pass" { + pass_count += 1; + } else { + fail_count += 1; + } + println!("{}", result); + eprintln!("{:<20} {}", format!("test {}", full_name), sw_one.elapsed()); + } + println!("{pass_count} passed, {fail_count} failed, {ignore_count} ignored"); + eprintln!("{:<20} {}", "All tests", sw_all.elapsed()); + Ok(()) + } +} + +fn all_modules(db: &dyn HirDatabase) -> Vec { + let mut worklist: Vec<_> = Crate::all(db) + .into_iter() + .filter(|x| x.origin(db).is_local()) + .map(|krate| krate.root_module(db)) + .collect(); + let mut modules = Vec::new(); + + while let Some(module) = worklist.pop() { + modules.push(module); + worklist.extend(module.children(db)); + } + + modules +}