Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: Add run-tests command #15110

Merged
merged 1 commit into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/hir-def/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,18 @@ impl Attrs {
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()
}
Expand Down
15 changes: 15 additions & 0 deletions crates/hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,21 @@ impl Function {
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)
}
Expand Down
21 changes: 8 additions & 13 deletions crates/ide/src/runnables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt;

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},
Expand All @@ -14,7 +14,7 @@ use ide_db::{
use itertools::Itertools;
use stdx::{always, format_to};
use syntax::{
ast::{self, AstNode, HasAttrs as _},
ast::{self, AstNode},
SmolStr, SyntaxNode,
};

Expand Down Expand Up @@ -307,7 +307,6 @@ pub(crate) fn runnable_fn(
sema: &Semantics<'_, RootDatabase>,
def: hir::Function,
) -> Option<Runnable> {
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);
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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) }
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
16 changes: 16 additions & 0 deletions crates/rust-analyzer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ mod diagnostics;
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)]
Expand Down Expand Up @@ -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("::")
}
22 changes: 4 additions & 18 deletions crates/rust-analyzer/src/cli/analysis_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use vfs::{AbsPathBuf, Vfs, VfsPath};

use crate::cli::{
flags::{self, OutputFormat},
full_name_of_item,
load_cargo::{load_workspace, LoadCargoConfig, ProcMacroServerChoice},
print_memory_usage,
progress_report::ProgressReport,
Expand Down Expand Up @@ -274,15 +275,7 @@ impl flags::AnalysisStats {
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;
Expand All @@ -304,15 +297,8 @@ impl flags::AnalysisStats {
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;
Expand Down
23 changes: 18 additions & 5 deletions crates/rust-analyzer/src/cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ xflags::xflags! {
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
Expand Down Expand Up @@ -147,6 +153,7 @@ pub enum RustAnalyzerCmd {
Symbols(Symbols),
Highlight(Highlight),
AnalysisStats(AnalysisStats),
RunTests(RunTests),
Diagnostics(Diagnostics),
Ssr(Ssr),
Search(Search),
Expand Down Expand Up @@ -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<String>,
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)]
Expand Down Expand Up @@ -223,6 +235,7 @@ pub struct Lsif {
#[derive(Debug)]
pub struct Scip {
pub path: PathBuf,

pub output: Option<PathBuf>,
}

Expand Down
92 changes: 92 additions & 0 deletions crates/rust-analyzer/src/cli/run_tests.rs
Original file line number Diff line number Diff line change
@@ -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("<unknown file>");
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<Module> {
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
}