Skip to content

Commit 674cd5a

Browse files
committed
Add run-tests command
1 parent f0e00ed commit 674cd5a

File tree

8 files changed

+166
-36
lines changed

8 files changed

+166
-36
lines changed

crates/hir-def/src/attr.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,18 @@ impl Attrs {
272272
self.by_key("proc_macro_derive").exists()
273273
}
274274

275+
pub fn is_test(&self) -> bool {
276+
self.by_key("test").exists()
277+
}
278+
279+
pub fn is_ignore(&self) -> bool {
280+
self.by_key("ignore").exists()
281+
}
282+
283+
pub fn is_bench(&self) -> bool {
284+
self.by_key("bench").exists()
285+
}
286+
275287
pub fn is_unstable(&self) -> bool {
276288
self.by_key("unstable").exists()
277289
}

crates/hir/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,6 +1927,21 @@ impl Function {
19271927
db.function_data(self.id).has_async_kw()
19281928
}
19291929

1930+
/// Does this function have `#[test]` attribute?
1931+
pub fn is_test(self, db: &dyn HirDatabase) -> bool {
1932+
db.function_data(self.id).attrs.is_test()
1933+
}
1934+
1935+
/// Does this function have the ignore attribute?
1936+
pub fn is_ignore(self, db: &dyn HirDatabase) -> bool {
1937+
db.function_data(self.id).attrs.is_ignore()
1938+
}
1939+
1940+
/// Does this function have `#[bench]` attribute?
1941+
pub fn is_bench(self, db: &dyn HirDatabase) -> bool {
1942+
db.function_data(self.id).attrs.is_bench()
1943+
}
1944+
19301945
pub fn is_unsafe_to_call(self, db: &dyn HirDatabase) -> bool {
19311946
hir_ty::is_fn_unsafe_to_call(db, self.id)
19321947
}

crates/ide/src/runnables.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::fmt;
22

33
use ast::HasName;
44
use cfg::CfgExpr;
5-
use hir::{AsAssocItem, HasAttrs, HasSource, Semantics};
5+
use hir::{db::HirDatabase, AsAssocItem, HasAttrs, HasSource, Semantics};
66
use ide_assists::utils::test_related_attribute;
77
use ide_db::{
88
base_db::{FilePosition, FileRange},
@@ -14,7 +14,7 @@ use ide_db::{
1414
use itertools::Itertools;
1515
use stdx::{always, format_to};
1616
use syntax::{
17-
ast::{self, AstNode, HasAttrs as _},
17+
ast::{self, AstNode},
1818
SmolStr, SyntaxNode,
1919
};
2020

@@ -307,7 +307,6 @@ pub(crate) fn runnable_fn(
307307
sema: &Semantics<'_, RootDatabase>,
308308
def: hir::Function,
309309
) -> Option<Runnable> {
310-
let func = def.source(sema.db)?;
311310
let name = def.name(sema.db).to_smol_str();
312311

313312
let root = def.module(sema.db).krate().root_module(sema.db);
@@ -323,10 +322,10 @@ pub(crate) fn runnable_fn(
323322
canonical_path.map(TestId::Path).unwrap_or(TestId::Name(name))
324323
};
325324

326-
if test_related_attribute(&func.value).is_some() {
327-
let attr = TestAttr::from_fn(&func.value);
325+
if def.is_test(sema.db) {
326+
let attr = TestAttr::from_fn(sema.db, def);
328327
RunnableKind::Test { test_id: test_id(), attr }
329-
} else if func.value.has_atom_attr("bench") {
328+
} else if def.is_bench(sema.db) {
330329
RunnableKind::Bench { test_id: test_id() }
331330
} else {
332331
return None;
@@ -335,7 +334,7 @@ pub(crate) fn runnable_fn(
335334

336335
let nav = NavigationTarget::from_named(
337336
sema.db,
338-
func.as_ref().map(|it| it as &dyn ast::HasName),
337+
def.source(sema.db)?.as_ref().map(|it| it as &dyn ast::HasName),
339338
SymbolKind::Function,
340339
);
341340
let cfg = def.attrs(sema.db).cfg();
@@ -487,12 +486,8 @@ pub struct TestAttr {
487486
}
488487

489488
impl TestAttr {
490-
fn from_fn(fn_def: &ast::Fn) -> TestAttr {
491-
let ignore = fn_def
492-
.attrs()
493-
.filter_map(|attr| attr.simple_name())
494-
.any(|attribute_text| attribute_text == "ignore");
495-
TestAttr { ignore }
489+
fn from_fn(db: &dyn HirDatabase, fn_def: hir::Function) -> TestAttr {
490+
TestAttr { ignore: fn_def.is_ignore(db) }
496491
}
497492
}
498493

crates/rust-analyzer/src/bin/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ fn main() -> anyhow::Result<()> {
8282
flags::RustAnalyzerCmd::Search(cmd) => cmd.run()?,
8383
flags::RustAnalyzerCmd::Lsif(cmd) => cmd.run()?,
8484
flags::RustAnalyzerCmd::Scip(cmd) => cmd.run()?,
85+
flags::RustAnalyzerCmd::RunTests(cmd) => cmd.run()?,
8586
}
8687
Ok(())
8788
}

crates/rust-analyzer/src/cli.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ mod diagnostics;
1010
mod ssr;
1111
mod lsif;
1212
mod scip;
13+
mod run_tests;
1314

1415
mod progress_report;
1516

1617
use std::io::Read;
1718

19+
use anyhow::Result;
20+
use hir::{Module, Name};
21+
use hir_ty::db::HirDatabase;
1822
use ide::AnalysisHost;
23+
use itertools::Itertools;
1924
use vfs::Vfs;
2025

2126
#[derive(Clone, Copy)]
@@ -70,3 +75,14 @@ fn print_memory_usage(mut host: AnalysisHost, vfs: Vfs) {
7075

7176
eprintln!("{remaining:>8} Remaining");
7277
}
78+
79+
fn full_name_of_item(db: &dyn HirDatabase, module: Module, name: Name) -> String {
80+
module
81+
.path_to_root(db)
82+
.into_iter()
83+
.rev()
84+
.filter_map(|it| it.name(db))
85+
.chain(Some(name))
86+
.map(|it| it.display(db.upcast()).to_string())
87+
.join("::")
88+
}

crates/rust-analyzer/src/cli/analysis_stats.rs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use vfs::{AbsPathBuf, Vfs, VfsPath};
3434

3535
use crate::cli::{
3636
flags::{self, OutputFormat},
37+
full_name_of_item,
3738
load_cargo::{load_workspace, LoadCargoConfig, ProcMacroServerChoice},
3839
print_memory_usage,
3940
progress_report::ProgressReport,
@@ -274,15 +275,7 @@ impl flags::AnalysisStats {
274275
continue
275276
};
276277
if verbosity.is_spammy() {
277-
let full_name = a
278-
.module(db)
279-
.path_to_root(db)
280-
.into_iter()
281-
.rev()
282-
.filter_map(|it| it.name(db))
283-
.chain(Some(a.name(db)))
284-
.map(|it| it.display(db).to_string())
285-
.join("::");
278+
let full_name = full_name_of_item(db, a.module(db), a.name(db));
286279
println!("Data layout for {full_name} failed due {e:?}");
287280
}
288281
fail += 1;
@@ -304,15 +297,8 @@ impl flags::AnalysisStats {
304297
continue;
305298
};
306299
if verbosity.is_spammy() {
307-
let full_name = c
308-
.module(db)
309-
.path_to_root(db)
310-
.into_iter()
311-
.rev()
312-
.filter_map(|it| it.name(db))
313-
.chain(c.name(db))
314-
.map(|it| it.display(db).to_string())
315-
.join("::");
300+
let full_name =
301+
full_name_of_item(db, c.module(db), c.name(db).unwrap_or(Name::missing()));
316302
println!("Const eval for {full_name} failed due {e:?}");
317303
}
318304
fail += 1;

crates/rust-analyzer/src/cli/flags.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ xflags::xflags! {
9090
optional --skip-const-eval
9191
}
9292

93+
/// Run unit tests of the project using mir interpreter
94+
cmd run-tests {
95+
/// Directory with Cargo.toml.
96+
required path: PathBuf
97+
}
98+
9399
cmd diagnostics {
94100
/// Directory with Cargo.toml.
95101
required path: PathBuf
@@ -147,6 +153,7 @@ pub enum RustAnalyzerCmd {
147153
Symbols(Symbols),
148154
Highlight(Highlight),
149155
AnalysisStats(AnalysisStats),
156+
RunTests(RunTests),
150157
Diagnostics(Diagnostics),
151158
Ssr(Ssr),
152159
Search(Search),
@@ -182,16 +189,21 @@ pub struct AnalysisStats {
182189
pub parallel: bool,
183190
pub memory_usage: bool,
184191
pub source_stats: bool,
185-
pub skip_lowering: bool,
186-
pub skip_inference: bool,
187-
pub skip_mir_stats: bool,
188-
pub skip_data_layout: bool,
189-
pub skip_const_eval: bool,
190192
pub only: Option<String>,
191193
pub with_deps: bool,
192194
pub no_sysroot: bool,
193195
pub disable_build_scripts: bool,
194196
pub disable_proc_macros: bool,
197+
pub skip_lowering: bool,
198+
pub skip_inference: bool,
199+
pub skip_mir_stats: bool,
200+
pub skip_data_layout: bool,
201+
pub skip_const_eval: bool,
202+
}
203+
204+
#[derive(Debug)]
205+
pub struct RunTests {
206+
pub path: PathBuf,
195207
}
196208

197209
#[derive(Debug)]
@@ -223,6 +235,7 @@ pub struct Lsif {
223235
#[derive(Debug)]
224236
pub struct Scip {
225237
pub path: PathBuf,
238+
226239
pub output: Option<PathBuf>,
227240
}
228241

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//! Run all tests in a project, similar to `cargo test`, but using the mir interpreter.
2+
3+
use hir::{Crate, Module};
4+
use hir_ty::db::HirDatabase;
5+
use ide_db::{base_db::SourceDatabaseExt, LineIndexDatabase};
6+
use profile::StopWatch;
7+
use project_model::{CargoConfig, RustLibSource};
8+
use syntax::TextRange;
9+
10+
use crate::cli::{
11+
flags, full_name_of_item,
12+
load_cargo::load_workspace_at,
13+
load_cargo::{LoadCargoConfig, ProcMacroServerChoice},
14+
Result,
15+
};
16+
17+
impl flags::RunTests {
18+
pub fn run(self) -> Result<()> {
19+
let mut cargo_config = CargoConfig::default();
20+
cargo_config.sysroot = Some(RustLibSource::Discover);
21+
let load_cargo_config = LoadCargoConfig {
22+
load_out_dirs_from_check: true,
23+
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
24+
prefill_caches: false,
25+
};
26+
let (host, _vfs, _proc_macro) =
27+
load_workspace_at(&self.path, &cargo_config, &load_cargo_config, &|_| {})?;
28+
let db = host.raw_database();
29+
30+
let tests = all_modules(db)
31+
.into_iter()
32+
.flat_map(|x| x.declarations(db))
33+
.filter_map(|x| match x {
34+
hir::ModuleDef::Function(f) => Some(f),
35+
_ => None,
36+
})
37+
.filter(|x| x.is_test(db));
38+
let span_formatter = |file_id, text_range: TextRange| {
39+
let line_col = match db.line_index(file_id).try_line_col(text_range.start()) {
40+
None => " (unknown line col)".to_string(),
41+
Some(x) => format!("#{}:{}", x.line + 1, x.col),
42+
};
43+
let path = &db
44+
.source_root(db.file_source_root(file_id))
45+
.path_for_file(&file_id)
46+
.map(|x| x.to_string());
47+
let path = path.as_deref().unwrap_or("<unknown file>");
48+
format!("file://{path}{line_col}")
49+
};
50+
let mut pass_count = 0;
51+
let mut ignore_count = 0;
52+
let mut fail_count = 0;
53+
let mut sw_all = StopWatch::start();
54+
for test in tests {
55+
let full_name = full_name_of_item(db, test.module(db), test.name(db));
56+
println!("test {}", full_name);
57+
if test.is_ignore(db) {
58+
println!("ignored");
59+
ignore_count += 1;
60+
continue;
61+
}
62+
let mut sw_one = StopWatch::start();
63+
let result = test.eval(db, span_formatter);
64+
if result.trim() == "pass" {
65+
pass_count += 1;
66+
} else {
67+
fail_count += 1;
68+
}
69+
println!("{}", result);
70+
eprintln!("{:<20} {}", format!("test {}", full_name), sw_one.elapsed());
71+
}
72+
println!("{pass_count} passed, {fail_count} failed, {ignore_count} ignored");
73+
eprintln!("{:<20} {}", "All tests", sw_all.elapsed());
74+
Ok(())
75+
}
76+
}
77+
78+
fn all_modules(db: &dyn HirDatabase) -> Vec<Module> {
79+
let mut worklist: Vec<_> = Crate::all(db)
80+
.into_iter()
81+
.filter(|x| x.origin(db).is_local())
82+
.map(|krate| krate.root_module(db))
83+
.collect();
84+
let mut modules = Vec::new();
85+
86+
while let Some(module) = worklist.pop() {
87+
modules.push(module);
88+
worklist.extend(module.children(db));
89+
}
90+
91+
modules
92+
}

0 commit comments

Comments
 (0)