1 //! A set of high-level utility fixture methods to use in tests.
2 use std::{mem, str::FromStr, sync};
3
4 use cfg::CfgOptions;
5 use rustc_hash::FxHashMap;
6 use test_utils::{
7 extract_range_or_offset, Fixture, FixtureWithProjectMeta, RangeOrOffset, CURSOR_MARKER,
8 ESCAPED_CURSOR_MARKER,
9 };
10 use triomphe::Arc;
11 use tt::token_id::{Leaf, Subtree, TokenTree};
12 use vfs::{file_set::FileSet, VfsPath};
13
14 use crate::{
15 input::{CrateName, CrateOrigin, LangCrateOrigin},
16 Change, CrateDisplayName, CrateGraph, CrateId, Dependency, Edition, Env, FileId, FilePosition,
17 FileRange, ProcMacro, ProcMacroExpander, ProcMacroExpansionError, ProcMacros, ReleaseChannel,
18 SourceDatabaseExt, SourceRoot, SourceRootId,
19 };
20
21 pub const WORKSPACE: SourceRootId = SourceRootId(0);
22
23 pub trait WithFixture: Default + SourceDatabaseExt + 'static {
24 #[track_caller]
with_single_file(ra_fixture: &str) -> (Self, FileId)25 fn with_single_file(ra_fixture: &str) -> (Self, FileId) {
26 let fixture = ChangeFixture::parse(ra_fixture);
27 let mut db = Self::default();
28 fixture.change.apply(&mut db);
29 assert_eq!(fixture.files.len(), 1);
30 (db, fixture.files[0])
31 }
32
33 #[track_caller]
with_many_files(ra_fixture: &str) -> (Self, Vec<FileId>)34 fn with_many_files(ra_fixture: &str) -> (Self, Vec<FileId>) {
35 let fixture = ChangeFixture::parse(ra_fixture);
36 let mut db = Self::default();
37 fixture.change.apply(&mut db);
38 assert!(fixture.file_position.is_none());
39 (db, fixture.files)
40 }
41
42 #[track_caller]
with_files(ra_fixture: &str) -> Self43 fn with_files(ra_fixture: &str) -> Self {
44 let fixture = ChangeFixture::parse(ra_fixture);
45 let mut db = Self::default();
46 fixture.change.apply(&mut db);
47 assert!(fixture.file_position.is_none());
48 db
49 }
50
51 #[track_caller]
with_files_extra_proc_macros( ra_fixture: &str, proc_macros: Vec<(String, ProcMacro)>, ) -> Self52 fn with_files_extra_proc_macros(
53 ra_fixture: &str,
54 proc_macros: Vec<(String, ProcMacro)>,
55 ) -> Self {
56 let fixture = ChangeFixture::parse_with_proc_macros(ra_fixture, proc_macros);
57 let mut db = Self::default();
58 fixture.change.apply(&mut db);
59 assert!(fixture.file_position.is_none());
60 db
61 }
62
63 #[track_caller]
with_position(ra_fixture: &str) -> (Self, FilePosition)64 fn with_position(ra_fixture: &str) -> (Self, FilePosition) {
65 let (db, file_id, range_or_offset) = Self::with_range_or_offset(ra_fixture);
66 let offset = range_or_offset.expect_offset();
67 (db, FilePosition { file_id, offset })
68 }
69
70 #[track_caller]
with_range(ra_fixture: &str) -> (Self, FileRange)71 fn with_range(ra_fixture: &str) -> (Self, FileRange) {
72 let (db, file_id, range_or_offset) = Self::with_range_or_offset(ra_fixture);
73 let range = range_or_offset.expect_range();
74 (db, FileRange { file_id, range })
75 }
76
77 #[track_caller]
with_range_or_offset(ra_fixture: &str) -> (Self, FileId, RangeOrOffset)78 fn with_range_or_offset(ra_fixture: &str) -> (Self, FileId, RangeOrOffset) {
79 let fixture = ChangeFixture::parse(ra_fixture);
80 let mut db = Self::default();
81 fixture.change.apply(&mut db);
82 let (file_id, range_or_offset) = fixture
83 .file_position
84 .expect("Could not find file position in fixture. Did you forget to add an `$0`?");
85 (db, file_id, range_or_offset)
86 }
87
test_crate(&self) -> CrateId88 fn test_crate(&self) -> CrateId {
89 let crate_graph = self.crate_graph();
90 let mut it = crate_graph.iter();
91 let res = it.next().unwrap();
92 assert!(it.next().is_none());
93 res
94 }
95 }
96
97 impl<DB: SourceDatabaseExt + Default + 'static> WithFixture for DB {}
98
99 pub struct ChangeFixture {
100 pub file_position: Option<(FileId, RangeOrOffset)>,
101 pub files: Vec<FileId>,
102 pub change: Change,
103 }
104
105 impl ChangeFixture {
parse(ra_fixture: &str) -> ChangeFixture106 pub fn parse(ra_fixture: &str) -> ChangeFixture {
107 Self::parse_with_proc_macros(ra_fixture, Vec::new())
108 }
109
parse_with_proc_macros( ra_fixture: &str, mut proc_macro_defs: Vec<(String, ProcMacro)>, ) -> ChangeFixture110 pub fn parse_with_proc_macros(
111 ra_fixture: &str,
112 mut proc_macro_defs: Vec<(String, ProcMacro)>,
113 ) -> ChangeFixture {
114 let FixtureWithProjectMeta { fixture, mini_core, proc_macro_names, toolchain } =
115 FixtureWithProjectMeta::parse(ra_fixture);
116 let toolchain = toolchain
117 .map(|it| {
118 ReleaseChannel::from_str(&it)
119 .unwrap_or_else(|| panic!("unknown release channel found: {it}"))
120 })
121 .unwrap_or(ReleaseChannel::Stable);
122 let mut change = Change::new();
123
124 let mut files = Vec::new();
125 let mut crate_graph = CrateGraph::default();
126 let mut crates = FxHashMap::default();
127 let mut crate_deps = Vec::new();
128 let mut default_crate_root: Option<FileId> = None;
129 let mut default_target_data_layout: Option<String> = None;
130 let mut default_cfg = CfgOptions::default();
131
132 let mut file_set = FileSet::default();
133 let mut current_source_root_kind = SourceRootKind::Local;
134 let source_root_prefix = "/".to_string();
135 let mut file_id = FileId(0);
136 let mut roots = Vec::new();
137
138 let mut file_position = None;
139
140 for entry in fixture {
141 let text = if entry.text.contains(CURSOR_MARKER) {
142 if entry.text.contains(ESCAPED_CURSOR_MARKER) {
143 entry.text.replace(ESCAPED_CURSOR_MARKER, CURSOR_MARKER)
144 } else {
145 let (range_or_offset, text) = extract_range_or_offset(&entry.text);
146 assert!(file_position.is_none());
147 file_position = Some((file_id, range_or_offset));
148 text
149 }
150 } else {
151 entry.text.clone()
152 };
153
154 let meta = FileMeta::from(entry);
155 assert!(meta.path.starts_with(&source_root_prefix));
156 if !meta.deps.is_empty() {
157 assert!(meta.krate.is_some(), "can't specify deps without naming the crate")
158 }
159
160 if let Some(kind) = &meta.introduce_new_source_root {
161 let root = match current_source_root_kind {
162 SourceRootKind::Local => SourceRoot::new_local(mem::take(&mut file_set)),
163 SourceRootKind::Library => SourceRoot::new_library(mem::take(&mut file_set)),
164 };
165 roots.push(root);
166 current_source_root_kind = *kind;
167 }
168
169 if let Some((krate, origin, version)) = meta.krate {
170 let crate_name = CrateName::normalize_dashes(&krate);
171 let crate_id = crate_graph.add_crate_root(
172 file_id,
173 meta.edition,
174 Some(crate_name.clone().into()),
175 version,
176 meta.cfg,
177 Default::default(),
178 meta.env,
179 false,
180 origin,
181 meta.target_data_layout
182 .as_deref()
183 .map(Arc::from)
184 .ok_or_else(|| "target_data_layout unset".into()),
185 Some(toolchain),
186 );
187 let prev = crates.insert(crate_name.clone(), crate_id);
188 assert!(prev.is_none());
189 for dep in meta.deps {
190 let prelude = meta.extern_prelude.contains(&dep);
191 let dep = CrateName::normalize_dashes(&dep);
192 crate_deps.push((crate_name.clone(), dep, prelude))
193 }
194 } else if meta.path == "/main.rs" || meta.path == "/lib.rs" {
195 assert!(default_crate_root.is_none());
196 default_crate_root = Some(file_id);
197 default_cfg = meta.cfg;
198 default_target_data_layout = meta.target_data_layout;
199 }
200
201 change.change_file(file_id, Some(Arc::from(text)));
202 let path = VfsPath::new_virtual_path(meta.path);
203 file_set.insert(file_id, path);
204 files.push(file_id);
205 file_id.0 += 1;
206 }
207
208 if crates.is_empty() {
209 let crate_root = default_crate_root
210 .expect("missing default crate root, specify a main.rs or lib.rs");
211 crate_graph.add_crate_root(
212 crate_root,
213 Edition::CURRENT,
214 Some(CrateName::new("test").unwrap().into()),
215 None,
216 default_cfg,
217 Default::default(),
218 Env::new_for_test_fixture(),
219 false,
220 CrateOrigin::Local { repo: None, name: None },
221 default_target_data_layout
222 .map(|x| x.into())
223 .ok_or_else(|| "target_data_layout unset".into()),
224 Some(toolchain),
225 );
226 } else {
227 for (from, to, prelude) in crate_deps {
228 let from_id = crates[&from];
229 let to_id = crates[&to];
230 crate_graph
231 .add_dep(
232 from_id,
233 Dependency::with_prelude(CrateName::new(&to).unwrap(), to_id, prelude),
234 )
235 .unwrap();
236 }
237 }
238 let target_layout = crate_graph.iter().next().map_or_else(
239 || Err("target_data_layout unset".into()),
240 |it| crate_graph[it].target_layout.clone(),
241 );
242
243 if let Some(mini_core) = mini_core {
244 let core_file = file_id;
245 file_id.0 += 1;
246
247 let mut fs = FileSet::default();
248 fs.insert(core_file, VfsPath::new_virtual_path("/sysroot/core/lib.rs".to_string()));
249 roots.push(SourceRoot::new_library(fs));
250
251 change.change_file(core_file, Some(Arc::from(mini_core.source_code())));
252
253 let all_crates = crate_graph.crates_in_topological_order();
254
255 let core_crate = crate_graph.add_crate_root(
256 core_file,
257 Edition::Edition2021,
258 Some(CrateDisplayName::from_canonical_name("core".to_string())),
259 None,
260 Default::default(),
261 Default::default(),
262 Env::new_for_test_fixture(),
263 false,
264 CrateOrigin::Lang(LangCrateOrigin::Core),
265 target_layout.clone(),
266 Some(toolchain),
267 );
268
269 for krate in all_crates {
270 crate_graph
271 .add_dep(krate, Dependency::new(CrateName::new("core").unwrap(), core_crate))
272 .unwrap();
273 }
274 }
275
276 let mut proc_macros = ProcMacros::default();
277 if !proc_macro_names.is_empty() {
278 let proc_lib_file = file_id;
279 file_id.0 += 1;
280
281 proc_macro_defs.extend(default_test_proc_macros());
282 let (proc_macro, source) = filter_test_proc_macros(&proc_macro_names, proc_macro_defs);
283 let mut fs = FileSet::default();
284 fs.insert(
285 proc_lib_file,
286 VfsPath::new_virtual_path("/sysroot/proc_macros/lib.rs".to_string()),
287 );
288 roots.push(SourceRoot::new_library(fs));
289
290 change.change_file(proc_lib_file, Some(Arc::from(source)));
291
292 let all_crates = crate_graph.crates_in_topological_order();
293
294 let proc_macros_crate = crate_graph.add_crate_root(
295 proc_lib_file,
296 Edition::Edition2021,
297 Some(CrateDisplayName::from_canonical_name("proc_macros".to_string())),
298 None,
299 Default::default(),
300 Default::default(),
301 Env::new_for_test_fixture(),
302 true,
303 CrateOrigin::Local { repo: None, name: None },
304 target_layout,
305 Some(toolchain),
306 );
307 proc_macros.insert(proc_macros_crate, Ok(proc_macro));
308
309 for krate in all_crates {
310 crate_graph
311 .add_dep(
312 krate,
313 Dependency::new(CrateName::new("proc_macros").unwrap(), proc_macros_crate),
314 )
315 .unwrap();
316 }
317 }
318
319 let root = match current_source_root_kind {
320 SourceRootKind::Local => SourceRoot::new_local(mem::take(&mut file_set)),
321 SourceRootKind::Library => SourceRoot::new_library(mem::take(&mut file_set)),
322 };
323 roots.push(root);
324 change.set_roots(roots);
325 change.set_crate_graph(crate_graph);
326 change.set_proc_macros(proc_macros);
327
328 ChangeFixture { file_position, files, change }
329 }
330 }
331
default_test_proc_macros() -> [(String, ProcMacro); 5]332 fn default_test_proc_macros() -> [(String, ProcMacro); 5] {
333 [
334 (
335 r#"
336 #[proc_macro_attribute]
337 pub fn identity(_attr: TokenStream, item: TokenStream) -> TokenStream {
338 item
339 }
340 "#
341 .into(),
342 ProcMacro {
343 name: "identity".into(),
344 kind: crate::ProcMacroKind::Attr,
345 expander: sync::Arc::new(IdentityProcMacroExpander),
346 },
347 ),
348 (
349 r#"
350 #[proc_macro_derive(DeriveIdentity)]
351 pub fn derive_identity(item: TokenStream) -> TokenStream {
352 item
353 }
354 "#
355 .into(),
356 ProcMacro {
357 name: "DeriveIdentity".into(),
358 kind: crate::ProcMacroKind::CustomDerive,
359 expander: sync::Arc::new(IdentityProcMacroExpander),
360 },
361 ),
362 (
363 r#"
364 #[proc_macro_attribute]
365 pub fn input_replace(attr: TokenStream, _item: TokenStream) -> TokenStream {
366 attr
367 }
368 "#
369 .into(),
370 ProcMacro {
371 name: "input_replace".into(),
372 kind: crate::ProcMacroKind::Attr,
373 expander: sync::Arc::new(AttributeInputReplaceProcMacroExpander),
374 },
375 ),
376 (
377 r#"
378 #[proc_macro]
379 pub fn mirror(input: TokenStream) -> TokenStream {
380 input
381 }
382 "#
383 .into(),
384 ProcMacro {
385 name: "mirror".into(),
386 kind: crate::ProcMacroKind::FuncLike,
387 expander: sync::Arc::new(MirrorProcMacroExpander),
388 },
389 ),
390 (
391 r#"
392 #[proc_macro]
393 pub fn shorten(input: TokenStream) -> TokenStream {
394 loop {}
395 }
396 "#
397 .into(),
398 ProcMacro {
399 name: "shorten".into(),
400 kind: crate::ProcMacroKind::FuncLike,
401 expander: sync::Arc::new(ShortenProcMacroExpander),
402 },
403 ),
404 ]
405 }
406
filter_test_proc_macros( proc_macro_names: &[String], proc_macro_defs: Vec<(String, ProcMacro)>, ) -> (Vec<ProcMacro>, String)407 fn filter_test_proc_macros(
408 proc_macro_names: &[String],
409 proc_macro_defs: Vec<(String, ProcMacro)>,
410 ) -> (Vec<ProcMacro>, String) {
411 // The source here is only required so that paths to the macros exist and are resolvable.
412 let mut source = String::new();
413 let mut proc_macros = Vec::new();
414
415 for (c, p) in proc_macro_defs {
416 if !proc_macro_names.iter().any(|name| name == &stdx::to_lower_snake_case(&p.name)) {
417 continue;
418 }
419 proc_macros.push(p);
420 source += &c;
421 }
422
423 (proc_macros, source)
424 }
425
426 #[derive(Debug, Clone, Copy)]
427 enum SourceRootKind {
428 Local,
429 Library,
430 }
431
432 #[derive(Debug)]
433 struct FileMeta {
434 path: String,
435 krate: Option<(String, CrateOrigin, Option<String>)>,
436 deps: Vec<String>,
437 extern_prelude: Vec<String>,
438 cfg: CfgOptions,
439 edition: Edition,
440 env: Env,
441 introduce_new_source_root: Option<SourceRootKind>,
442 target_data_layout: Option<String>,
443 }
444
parse_crate(crate_str: String) -> (String, CrateOrigin, Option<String>)445 fn parse_crate(crate_str: String) -> (String, CrateOrigin, Option<String>) {
446 if let Some((a, b)) = crate_str.split_once('@') {
447 let (version, origin) = match b.split_once(':') {
448 Some(("CratesIo", data)) => match data.split_once(',') {
449 Some((version, url)) => {
450 (version, CrateOrigin::Local { repo: Some(url.to_owned()), name: None })
451 }
452 _ => panic!("Bad crates.io parameter: {data}"),
453 },
454 _ => panic!("Bad string for crate origin: {b}"),
455 };
456 (a.to_owned(), origin, Some(version.to_string()))
457 } else {
458 let crate_origin = match LangCrateOrigin::from(&*crate_str) {
459 LangCrateOrigin::Other => CrateOrigin::Local { repo: None, name: None },
460 origin => CrateOrigin::Lang(origin),
461 };
462 (crate_str, crate_origin, None)
463 }
464 }
465
466 impl From<Fixture> for FileMeta {
from(f: Fixture) -> FileMeta467 fn from(f: Fixture) -> FileMeta {
468 let mut cfg = CfgOptions::default();
469 f.cfg_atoms.iter().for_each(|it| cfg.insert_atom(it.into()));
470 f.cfg_key_values.iter().for_each(|(k, v)| cfg.insert_key_value(k.into(), v.into()));
471 let deps = f.deps;
472 FileMeta {
473 path: f.path,
474 krate: f.krate.map(parse_crate),
475 extern_prelude: f.extern_prelude.unwrap_or_else(|| deps.clone()),
476 deps,
477 cfg,
478 edition: f.edition.as_ref().map_or(Edition::CURRENT, |v| Edition::from_str(v).unwrap()),
479 env: f.env.into_iter().collect(),
480 introduce_new_source_root: f.introduce_new_source_root.map(|kind| match &*kind {
481 "local" => SourceRootKind::Local,
482 "library" => SourceRootKind::Library,
483 invalid => panic!("invalid source root kind '{invalid}'"),
484 }),
485 target_data_layout: f.target_data_layout,
486 }
487 }
488 }
489
490 // Identity mapping
491 #[derive(Debug)]
492 struct IdentityProcMacroExpander;
493 impl ProcMacroExpander for IdentityProcMacroExpander {
expand( &self, subtree: &Subtree, _: Option<&Subtree>, _: &Env, ) -> Result<Subtree, ProcMacroExpansionError>494 fn expand(
495 &self,
496 subtree: &Subtree,
497 _: Option<&Subtree>,
498 _: &Env,
499 ) -> Result<Subtree, ProcMacroExpansionError> {
500 Ok(subtree.clone())
501 }
502 }
503
504 // Pastes the attribute input as its output
505 #[derive(Debug)]
506 struct AttributeInputReplaceProcMacroExpander;
507 impl ProcMacroExpander for AttributeInputReplaceProcMacroExpander {
expand( &self, _: &Subtree, attrs: Option<&Subtree>, _: &Env, ) -> Result<Subtree, ProcMacroExpansionError>508 fn expand(
509 &self,
510 _: &Subtree,
511 attrs: Option<&Subtree>,
512 _: &Env,
513 ) -> Result<Subtree, ProcMacroExpansionError> {
514 attrs
515 .cloned()
516 .ok_or_else(|| ProcMacroExpansionError::Panic("Expected attribute input".into()))
517 }
518 }
519
520 #[derive(Debug)]
521 struct MirrorProcMacroExpander;
522 impl ProcMacroExpander for MirrorProcMacroExpander {
expand( &self, input: &Subtree, _: Option<&Subtree>, _: &Env, ) -> Result<Subtree, ProcMacroExpansionError>523 fn expand(
524 &self,
525 input: &Subtree,
526 _: Option<&Subtree>,
527 _: &Env,
528 ) -> Result<Subtree, ProcMacroExpansionError> {
529 fn traverse(input: &Subtree) -> Subtree {
530 let mut token_trees = vec![];
531 for tt in input.token_trees.iter().rev() {
532 let tt = match tt {
533 tt::TokenTree::Leaf(leaf) => tt::TokenTree::Leaf(leaf.clone()),
534 tt::TokenTree::Subtree(sub) => tt::TokenTree::Subtree(traverse(sub)),
535 };
536 token_trees.push(tt);
537 }
538 Subtree { delimiter: input.delimiter, token_trees }
539 }
540 Ok(traverse(input))
541 }
542 }
543
544 // Replaces every literal with an empty string literal and every identifier with its first letter,
545 // but retains all tokens' span. Useful for testing we don't assume token hasn't been modified by
546 // macros even if it retains its span.
547 #[derive(Debug)]
548 struct ShortenProcMacroExpander;
549 impl ProcMacroExpander for ShortenProcMacroExpander {
expand( &self, input: &Subtree, _: Option<&Subtree>, _: &Env, ) -> Result<Subtree, ProcMacroExpansionError>550 fn expand(
551 &self,
552 input: &Subtree,
553 _: Option<&Subtree>,
554 _: &Env,
555 ) -> Result<Subtree, ProcMacroExpansionError> {
556 return Ok(traverse(input));
557
558 fn traverse(input: &Subtree) -> Subtree {
559 let token_trees = input
560 .token_trees
561 .iter()
562 .map(|it| match it {
563 TokenTree::Leaf(leaf) => tt::TokenTree::Leaf(modify_leaf(leaf)),
564 TokenTree::Subtree(subtree) => tt::TokenTree::Subtree(traverse(subtree)),
565 })
566 .collect();
567 Subtree { delimiter: input.delimiter, token_trees }
568 }
569
570 fn modify_leaf(leaf: &Leaf) -> Leaf {
571 let mut leaf = leaf.clone();
572 match &mut leaf {
573 Leaf::Literal(it) => {
574 // XXX Currently replaces any literals with an empty string, but supporting
575 // "shortening" other literals would be nice.
576 it.text = "\"\"".into();
577 }
578 Leaf::Punct(_) => {}
579 Leaf::Ident(it) => {
580 it.text = it.text.chars().take(1).collect();
581 }
582 }
583 leaf
584 }
585 }
586 }
587