test_utils/
fixture.rs

1//! Defines `Fixture` -- a convenient way to describe the initial state of
2//! rust-analyzer database from a single string.
3//!
4//! Fixtures are strings containing rust source code with optional metadata.
5//! A fixture without metadata is parsed into a single source file.
6//! Use this to test functionality local to one file.
7//!
8//! Simple Example:
9//!
10//! ```ignore
11//! r#"
12//! fn main() {
13//!     println!("Hello World")
14//! }
15//! "#
16//! ```
17//!
18//! Metadata can be added to a fixture after a `//-` comment.
19//! The basic form is specifying filenames,
20//! which is also how to define multiple files in a single test fixture
21//!
22//! Example using two files in the same crate:
23//!
24//! ```ignore
25//! "
26//! //- /main.rs
27//! mod foo;
28//! fn main() {
29//!     foo::bar();
30//! }
31//!
32//! //- /foo.rs
33//! pub fn bar() {}
34//! "
35//! ```
36//!
37//! Example using two crates with one file each, with one crate depending on the other:
38//!
39//! ```ignore
40//! r#"
41//! //- /main.rs crate:a deps:b
42//! fn main() {
43//!     b::foo();
44//! }
45//! //- /lib.rs crate:b
46//! pub fn b() {
47//!     println!("Hello World")
48//! }
49//! "#
50//! ```
51//!
52//! Metadata allows specifying all settings and variables
53//! that are available in a real rust project. See [`Fixture`]
54//! for the syntax.
55//!
56//! Example using some available metadata:
57//!
58//! ```ignore
59//! "
60//! //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
61//! fn insert_source_code_here() {}
62//! "
63//! ```
64
65use std::iter;
66
67use rustc_hash::FxHashMap;
68use stdx::trim_indent;
69
70#[derive(Debug, Eq, PartialEq)]
71pub struct Fixture {
72    /// Specifies the path for this file. It must start with "/".
73    pub path: String,
74    /// Defines a new crate and make this file its root module.
75    ///
76    /// Version and repository URL of the crate can optionally be specified; if
77    /// either one is specified, the other must also be specified.
78    ///
79    /// Syntax:
80    /// - `crate:my_awesome_lib`
81    /// - `crate:my_awesome_lib@0.0.1,https://example.com/repo.git`
82    pub krate: Option<String>,
83    /// Specifies dependencies of this crate. This must be used with `crate` meta.
84    ///
85    /// Syntax: `deps:hir-def,ide-assists`
86    pub deps: Vec<String>,
87    /// Limits crates in the extern prelude. The set of crate names must be a
88    /// subset of `deps`. This must be used with `crate` meta.
89    ///
90    /// If this is not specified, all the dependencies will be in the extern prelude.
91    ///
92    /// Syntax: `extern-prelude:hir-def,ide-assists`
93    pub extern_prelude: Option<Vec<String>>,
94    /// Specifies configuration options to be enabled. Options may have associated
95    /// values.
96    ///
97    /// Syntax: `cfg:test,dbg=false,opt_level=2`
98    pub cfgs: Vec<(String, Option<String>)>,
99    /// Specifies the edition of this crate. This must be used with
100    /// `crate` meta. If this is not specified,
101    /// `base_db::input::Edition::CURRENT` will be used.  This must be
102    /// used with `crate` meta.
103    ///
104    /// Syntax: `edition:2021`
105    pub edition: Option<String>,
106    /// Specifies environment variables.
107    ///
108    /// Syntax: `env:PATH=/bin,RUST_LOG=debug`
109    pub env: FxHashMap<String, String>,
110    /// Introduces a new source root. This file **and the following
111    /// files** will belong the new source root. This must be used
112    /// with `crate` meta.
113    ///
114    /// Use this if you want to test something that uses `SourceRoot::is_library()`
115    /// to check editability.
116    ///
117    /// Note that files before the first fixture with `new_source_root` meta will
118    /// belong to an implicitly defined local source root.
119    ///
120    /// Syntax:
121    /// - `new_source_root:library`
122    /// - `new_source_root:local`
123    pub introduce_new_source_root: Option<String>,
124    /// Explicitly declares this crate as a library outside current workspace. This
125    /// must be used with `crate` meta.
126    ///
127    /// This is implied if this file belongs to a library source root.
128    ///
129    /// Use this if you want to test something that checks if a crate is a workspace
130    /// member via `CrateOrigin`.
131    ///
132    /// Syntax: `library`
133    pub library: bool,
134    /// Actual file contents. All meta comments are stripped.
135    pub text: String,
136    /// The line number in the original fixture of the beginning of this fixture.
137    pub line: usize,
138}
139
140#[derive(Debug)]
141pub struct MiniCore {
142    activated_flags: Vec<String>,
143    valid_flags: Vec<String>,
144}
145
146#[derive(Debug)]
147pub struct FixtureWithProjectMeta {
148    pub fixture: Vec<Fixture>,
149    pub mini_core: Option<MiniCore>,
150    pub proc_macro_names: Vec<String>,
151    pub toolchain: Option<String>,
152    /// Specifies LLVM data layout to be used.
153    ///
154    /// You probably don't want to manually specify this. See LLVM manual for the
155    /// syntax, if you must: <https://llvm.org/docs/LangRef.html#data-layout>
156    pub target_data_layout: String,
157    /// Specifies the target architecture.
158    pub target_arch: String,
159}
160
161impl FixtureWithProjectMeta {
162    /// Parses text which looks like this:
163    ///
164    ///  ```text
165    ///  //- some meta
166    ///  line 1
167    ///  line 2
168    ///  //- other meta
169    ///  ```
170    ///
171    /// Fixture can also start with a proc_macros and minicore declaration (in that order):
172    ///
173    /// ```text
174    /// //- toolchain: nightly
175    /// //- proc_macros: identity
176    /// //- minicore: sized
177    /// ```
178    ///
179    /// That will set toolchain to nightly and include predefined proc macros and a subset of
180    /// `libcore` into the fixture, see `minicore.rs` for what's available. Note that toolchain
181    /// defaults to stable.
182    pub fn parse(#[rust_analyzer::rust_fixture] ra_fixture: &str) -> Self {
183        let fixture = trim_indent(ra_fixture);
184        let mut fixture = fixture.as_str();
185        let mut toolchain = None;
186        let mut target_data_layout =
187            "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128".to_owned();
188        let mut target_arch = "x86_64".to_owned();
189        let mut mini_core = None;
190        let mut res: Vec<Fixture> = Vec::new();
191        let mut proc_macro_names = vec![];
192        let mut first_row = 0;
193
194        if let Some(meta) = fixture.strip_prefix("//- toolchain:") {
195            first_row += 1;
196            let (meta, remain) = meta.split_once('\n').unwrap();
197            toolchain = Some(meta.trim().to_owned());
198            fixture = remain;
199        }
200
201        if let Some(meta) = fixture.strip_prefix("//- target_data_layout:") {
202            first_row += 1;
203            let (meta, remain) = meta.split_once('\n').unwrap();
204            meta.trim().clone_into(&mut target_data_layout);
205            fixture = remain;
206        }
207
208        if let Some(meta) = fixture.strip_prefix("//- target_arch:") {
209            first_row += 1;
210            let (meta, remain) = meta.split_once('\n').unwrap();
211            meta.trim().clone_into(&mut target_arch);
212            fixture = remain;
213        }
214
215        if let Some(meta) = fixture.strip_prefix("//- proc_macros:") {
216            first_row += 1;
217            let (meta, remain) = meta.split_once('\n').unwrap();
218            proc_macro_names = meta.split(',').map(|it| it.trim().to_owned()).collect();
219            fixture = remain;
220        }
221
222        if let Some(meta) = fixture.strip_prefix("//- minicore:") {
223            first_row += 1;
224            let (meta, remain) = meta.split_once('\n').unwrap();
225            mini_core = Some(MiniCore::parse(meta));
226            fixture = remain;
227        }
228
229        let default =
230            if fixture.contains("//- /") { None } else { Some((first_row - 1, "//- /main.rs")) };
231
232        for (ix, line) in
233            default.into_iter().chain((first_row..).zip(fixture.split_inclusive('\n')))
234        {
235            if line.contains("//-") {
236                assert!(
237                    line.starts_with("//-"),
238                    "Metadata line {ix} has invalid indentation. \
239                     All metadata lines need to have the same indentation.\n\
240                     The offending line: {line:?}"
241                );
242            }
243
244            if let Some(line) = line.strip_prefix("//-") {
245                let meta = Self::parse_meta_line(line, (ix + 1).try_into().unwrap());
246                res.push(meta);
247            } else {
248                if matches!(line.strip_prefix("// "), Some(l) if l.trim().starts_with('/')) {
249                    panic!("looks like invalid metadata line: {line:?}");
250                }
251
252                if let Some(entry) = res.last_mut() {
253                    entry.text.push_str(line);
254                }
255            }
256        }
257
258        Self {
259            fixture: res,
260            mini_core,
261            proc_macro_names,
262            toolchain,
263            target_data_layout,
264            target_arch,
265        }
266    }
267
268    //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
269    fn parse_meta_line(meta: &str, line: usize) -> Fixture {
270        let meta = meta.trim();
271        let mut components = meta.split_ascii_whitespace();
272
273        let path = components.next().expect("fixture meta must start with a path").to_owned();
274        assert!(path.starts_with('/'), "fixture path does not start with `/`: {path:?}");
275
276        let mut krate = None;
277        let mut deps = Vec::new();
278        let mut extern_prelude = None;
279        let mut edition = None;
280        let mut cfgs = Vec::new();
281        let mut env = FxHashMap::default();
282        let mut introduce_new_source_root = None;
283        let mut library = false;
284        for component in components {
285            if component == "library" {
286                library = true;
287                continue;
288            }
289
290            let (key, value) =
291                component.split_once(':').unwrap_or_else(|| panic!("invalid meta line: {meta:?}"));
292            match key {
293                "crate" => krate = Some(value.to_owned()),
294                "deps" => deps = value.split(',').map(|it| it.to_owned()).collect(),
295                "extern-prelude" => {
296                    if value.is_empty() {
297                        extern_prelude = Some(Vec::new());
298                    } else {
299                        extern_prelude =
300                            Some(value.split(',').map(|it| it.to_owned()).collect::<Vec<_>>());
301                    }
302                }
303                "edition" => edition = Some(value.to_owned()),
304                "cfg" => {
305                    for entry in value.split(',') {
306                        match entry.split_once('=') {
307                            Some((k, v)) => cfgs.push((k.to_owned(), Some(v.to_owned()))),
308                            None => cfgs.push((entry.to_owned(), None)),
309                        }
310                    }
311                }
312                "env" => {
313                    for key in value.split(',') {
314                        if let Some((k, v)) = key.split_once('=') {
315                            env.insert(k.into(), v.into());
316                        }
317                    }
318                }
319                "new_source_root" => introduce_new_source_root = Some(value.to_owned()),
320                _ => panic!("bad component: {component:?}"),
321            }
322        }
323
324        for prelude_dep in extern_prelude.iter().flatten() {
325            assert!(
326                deps.contains(prelude_dep),
327                "extern-prelude {extern_prelude:?} must be a subset of deps {deps:?}"
328            );
329        }
330
331        Fixture {
332            path,
333            text: String::new(),
334            line,
335            krate,
336            deps,
337            extern_prelude,
338            cfgs,
339            edition,
340            env,
341            introduce_new_source_root,
342            library,
343        }
344    }
345}
346
347impl MiniCore {
348    pub const RAW_SOURCE: &'static str = include_str!("./minicore.rs");
349
350    fn has_flag(&self, flag: &str) -> bool {
351        self.activated_flags.iter().any(|it| it == flag)
352    }
353
354    pub fn from_flags<'a>(flags: impl IntoIterator<Item = &'a str>) -> Self {
355        MiniCore {
356            activated_flags: flags.into_iter().map(|x| x.to_owned()).collect(),
357            valid_flags: Vec::new(),
358        }
359    }
360
361    #[track_caller]
362    fn assert_valid_flag(&self, flag: &str) {
363        if !self.valid_flags.iter().any(|it| it == flag) {
364            panic!("invalid flag: {flag:?}, valid flags: {:?}", self.valid_flags);
365        }
366    }
367
368    fn parse(line: &str) -> MiniCore {
369        let mut res = MiniCore { activated_flags: Vec::new(), valid_flags: Vec::new() };
370
371        for entry in line.trim().split(", ") {
372            if res.has_flag(entry) {
373                panic!("duplicate minicore flag: {entry:?}");
374            }
375            res.activated_flags.push(entry.to_owned());
376        }
377
378        res
379    }
380
381    pub fn available_flags(raw_source: &str) -> impl Iterator<Item = &str> {
382        let lines = raw_source.split_inclusive('\n');
383        lines
384            .map_while(|x| x.strip_prefix("//!"))
385            .skip_while(|line| !line.contains("Available flags:"))
386            .skip(1)
387            .map(|x| x.split_once(':').unwrap().0.trim())
388    }
389
390    /// Strips parts of minicore.rs which are flagged by inactive flags.
391    ///
392    /// This is probably over-engineered to support flags dependencies.
393    pub fn source_code(mut self, raw_source: &str) -> String {
394        let mut buf = String::new();
395        let mut lines = raw_source.split_inclusive('\n');
396
397        let mut implications = Vec::new();
398
399        // Parse `//!` preamble and extract flags and dependencies.
400        let trim_doc: fn(&str) -> Option<&str> = |line| match line.strip_prefix("//!") {
401            Some(it) => Some(it),
402            None => {
403                assert!(line.trim().is_empty(), "expected empty line after minicore header");
404                None
405            }
406        };
407        for line in lines
408            .by_ref()
409            .map_while(trim_doc)
410            .skip_while(|line| !line.contains("Available flags:"))
411            .skip(1)
412        {
413            let (flag, deps) = line.split_once(':').unwrap();
414            let flag = flag.trim();
415
416            self.valid_flags.push(flag.to_owned());
417            implications.extend(
418                iter::repeat(flag)
419                    .zip(deps.split(", ").map(str::trim).filter(|dep| !dep.is_empty())),
420            );
421        }
422
423        for (_, dep) in &implications {
424            self.assert_valid_flag(dep);
425        }
426
427        for flag in &self.activated_flags {
428            self.assert_valid_flag(flag);
429        }
430
431        // Fixed point loop to compute transitive closure of flags.
432        loop {
433            let mut changed = false;
434            for &(u, v) in &implications {
435                if self.has_flag(u) && !self.has_flag(v) {
436                    self.activated_flags.push(v.to_owned());
437                    changed = true;
438                }
439            }
440            if !changed {
441                break;
442            }
443        }
444
445        let mut active_regions = Vec::new();
446        let mut inactive_regions = Vec::new();
447        let mut seen_regions = Vec::new();
448        for line in lines {
449            let trimmed = line.trim();
450            if let Some(region) = trimmed.strip_prefix("// region:") {
451                if let Some(region) = region.strip_prefix('!') {
452                    inactive_regions.push(region);
453                    continue;
454                } else {
455                    active_regions.push(region);
456                    continue;
457                }
458            }
459            if let Some(region) = trimmed.strip_prefix("// endregion:") {
460                let (prev, region) = if let Some(region) = region.strip_prefix('!') {
461                    (inactive_regions.pop().unwrap(), region)
462                } else {
463                    (active_regions.pop().unwrap(), region)
464                };
465                assert_eq!(prev, region, "unbalanced region pairs");
466                continue;
467            }
468
469            let mut active_line_region = 0;
470            let mut inactive_line_region = 0;
471            if let Some(idx) = trimmed.find("// :!") {
472                let regions = trimmed[idx + "// :!".len()..].split(", ");
473                inactive_line_region += regions.clone().count();
474                inactive_regions.extend(regions);
475            } else if let Some(idx) = trimmed.find("// :") {
476                let regions = trimmed[idx + "// :".len()..].split(", ");
477                active_line_region += regions.clone().count();
478                active_regions.extend(regions);
479            }
480
481            let mut keep = true;
482            for &region in &active_regions {
483                assert!(!region.starts_with(' '), "region marker starts with a space: {region:?}");
484                self.assert_valid_flag(region);
485                seen_regions.push(region);
486                keep &= self.has_flag(region);
487            }
488            for &region in &inactive_regions {
489                assert!(!region.starts_with(' '), "region marker starts with a space: {region:?}");
490                self.assert_valid_flag(region);
491                seen_regions.push(region);
492                keep &= !self.has_flag(region);
493            }
494
495            if keep {
496                buf.push_str(line);
497            }
498            if active_line_region > 0 {
499                active_regions.drain(active_regions.len() - active_line_region..);
500            }
501            if inactive_line_region > 0 {
502                inactive_regions.drain(inactive_regions.len() - active_line_region..);
503            }
504        }
505
506        if !active_regions.is_empty() {
507            panic!("unclosed regions: {active_regions:?} Add an `endregion` comment");
508        }
509        if !inactive_regions.is_empty() {
510            panic!("unclosed regions: {inactive_regions:?} Add an `endregion` comment");
511        }
512
513        for flag in &self.valid_flags {
514            if !seen_regions.iter().any(|it| it == flag) {
515                panic!("unused minicore flag: {flag:?}");
516            }
517        }
518        buf
519    }
520}
521
522#[test]
523#[should_panic]
524fn parse_fixture_checks_further_indented_metadata() {
525    FixtureWithProjectMeta::parse(
526        r"
527        //- /lib.rs
528          mod bar;
529
530          fn foo() {}
531          //- /bar.rs
532          pub fn baz() {}
533          ",
534    );
535}
536
537#[test]
538fn parse_fixture_gets_full_meta() {
539    let FixtureWithProjectMeta {
540        fixture: parsed,
541        mini_core,
542        proc_macro_names,
543        toolchain,
544        target_data_layout: _,
545        target_arch: _,
546    } = FixtureWithProjectMeta::parse(
547        r#"
548//- toolchain: nightly
549//- proc_macros: identity
550//- minicore: coerce_unsized
551//- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b,atom env:OUTDIR=path/to,OTHER=foo
552mod m;
553"#,
554    );
555    assert_eq!(toolchain, Some("nightly".to_owned()));
556    assert_eq!(proc_macro_names, vec!["identity".to_owned()]);
557    assert_eq!(mini_core.unwrap().activated_flags, vec!["coerce_unsized".to_owned()]);
558    assert_eq!(1, parsed.len());
559
560    let meta = &parsed[0];
561    assert_eq!("mod m;\n", meta.text);
562
563    assert_eq!("foo", meta.krate.as_ref().unwrap());
564    assert_eq!("/lib.rs", meta.path);
565    assert_eq!(2, meta.env.len());
566}