diff --git a/crates/mun_hir/Cargo.toml b/crates/mun_hir/Cargo.toml index b50ae247a..6a8f156f7 100644 --- a/crates/mun_hir/Cargo.toml +++ b/crates/mun_hir/Cargo.toml @@ -27,3 +27,4 @@ either = "1.5.3" [dev-dependencies] insta = "0.16" parking_lot = "0.10" +mun_test = { version = "=0.1.0", path = "../mun_test" } diff --git a/crates/mun_hir/src/expr/validator/tests.rs b/crates/mun_hir/src/expr/validator/tests.rs index 42394f4bc..245cff823 100644 --- a/crates/mun_hir/src/expr/validator/tests.rs +++ b/crates/mun_hir/src/expr/validator/tests.rs @@ -2,6 +2,7 @@ use crate::{ db::DefDatabase, diagnostics::DiagnosticSink, expr::validator::{ExprValidator, TypeAliasValidator}, + fixture::WithFixture, mock::MockDatabase, ModuleDef, }; diff --git a/crates/mun_hir/src/fixture.rs b/crates/mun_hir/src/fixture.rs new file mode 100644 index 000000000..47b44f6db --- /dev/null +++ b/crates/mun_hir/src/fixture.rs @@ -0,0 +1,48 @@ +#![cfg(test)] + +use crate::{FileId, SourceDatabase, SourceRoot, SourceRootId}; +pub use mun_test::Fixture; +use std::convert::TryInto; +use std::sync::Arc; + +impl WithFixture for DB {} + +/// Enables the creation of an instance from a [`Fixture`] +pub trait WithFixture: Default + SourceDatabase + 'static { + /// Constructs an instance from a fixture + fn with_files(fixture: impl AsRef) -> Self { + let mut db = Self::default(); + with_files(&mut db, fixture.as_ref()); + db + } + + /// Constructs an instance from a fixture + fn with_single_file(text: impl AsRef) -> (Self, FileId) { + let mut db = Self::default(); + let files = with_files(&mut db, text.as_ref()); + assert_eq!(files.len(), 1); + (db, files[0]) + } +} + +/// Fills the specified database with all the files from the specified `fixture` +fn with_files(db: &mut dyn SourceDatabase, fixture: &str) -> Vec { + let fixture = Fixture::parse(fixture); + + let mut source_root = SourceRoot::default(); + let source_root_id = SourceRootId(0); + let mut files = Vec::new(); + + for (idx, entry) in fixture.into_iter().enumerate() { + let file_id = FileId(idx.try_into().expect("too many files")); + db.set_file_relative_path(file_id, entry.relative_path); + db.set_file_text(file_id, Arc::new(entry.text)); + db.set_file_source_root(file_id, source_root_id); + source_root.insert_file(file_id); + files.push(file_id); + } + + db.set_source_root(source_root_id, Arc::new(source_root)); + + return files; +} diff --git a/crates/mun_hir/src/item_tree/tests.rs b/crates/mun_hir/src/item_tree/tests.rs index 4e7a00cee..f6ec62ace 100644 --- a/crates/mun_hir/src/item_tree/tests.rs +++ b/crates/mun_hir/src/item_tree/tests.rs @@ -1,5 +1,6 @@ -use crate::item_tree::Fields; use crate::{ + fixture::WithFixture, + item_tree::Fields, item_tree::{ItemTree, ModItem}, mock::MockDatabase, DefDatabase, diff --git a/crates/mun_hir/src/lib.rs b/crates/mun_hir/src/lib.rs index 79651cd68..ab350ebf9 100644 --- a/crates/mun_hir/src/lib.rs +++ b/crates/mun_hir/src/lib.rs @@ -16,6 +16,7 @@ mod db; pub mod diagnostics; mod display; mod expr; +mod fixture; mod ids; mod in_file; mod input; diff --git a/crates/mun_hir/src/mock.rs b/crates/mun_hir/src/mock.rs index 73045afee..70621b75a 100644 --- a/crates/mun_hir/src/mock.rs +++ b/crates/mun_hir/src/mock.rs @@ -1,10 +1,12 @@ -use crate::db::{AstDatabase, SourceDatabase}; -use crate::db::{HirDatabase, Upcast}; -use crate::input::{SourceRoot, SourceRootId}; -use crate::{DefDatabase, FileId, RelativePathBuf}; +#![cfg(test)] + +use crate::{ + db::{AstDatabase, SourceDatabase}, + db::{HirDatabase, Upcast}, + DefDatabase, +}; use mun_target::spec::Target; use parking_lot::Mutex; -use std::sync::Arc; /// A mock implementation of the IR database. It can be used to set up a simple test case. #[salsa::database( @@ -14,7 +16,6 @@ use std::sync::Arc; crate::DefDatabaseStorage, crate::HirDatabaseStorage )] -#[derive(Default)] pub(crate) struct MockDatabase { storage: salsa::Storage, events: Mutex>>, @@ -47,25 +48,14 @@ impl Upcast for MockDatabase { } } -impl MockDatabase { - /// Creates a database from the given text. - pub fn with_single_file(text: &str) -> (MockDatabase, FileId) { - let mut db: MockDatabase = Default::default(); - - let mut source_root = SourceRoot::default(); - let source_root_id = SourceRootId(0); - - let text = Arc::new(text.to_owned()); - let rel_path = RelativePathBuf::from("main.mun"); - let file_id = FileId(0); +impl Default for MockDatabase { + fn default() -> Self { + let mut db: MockDatabase = MockDatabase { + storage: Default::default(), + events: Default::default(), + }; db.set_target(Target::host_target().unwrap()); - db.set_file_relative_path(file_id, rel_path.clone()); - db.set_file_text(file_id, Arc::new(text.to_string())); - db.set_file_source_root(file_id, source_root_id); - source_root.insert_file(file_id); - - db.set_source_root(source_root_id, Arc::new(source_root)); - (db, file_id) + db } } diff --git a/crates/mun_hir/src/tests.rs b/crates/mun_hir/src/tests.rs index 1dc5e0007..ec23813ea 100644 --- a/crates/mun_hir/src/tests.rs +++ b/crates/mun_hir/src/tests.rs @@ -1,5 +1,8 @@ -use crate::db::{DefDatabase, SourceDatabase}; -use crate::mock::MockDatabase; +use crate::{ + db::{DefDatabase, SourceDatabase}, + fixture::WithFixture, + mock::MockDatabase, +}; use std::sync::Arc; /// This function tests that the ModuleData of a module does not change if the contents of a function diff --git a/crates/mun_hir/src/ty/tests.rs b/crates/mun_hir/src/ty/tests.rs index 262b3678b..851f7d9b5 100644 --- a/crates/mun_hir/src/ty/tests.rs +++ b/crates/mun_hir/src/ty/tests.rs @@ -1,3 +1,4 @@ +use crate::fixture::WithFixture; use crate::{ db::DefDatabase, diagnostics::DiagnosticSink, expr::BodySourceMap, mock::MockDatabase, HirDisplay, InferenceResult, ModuleDef, diff --git a/crates/mun_test/Cargo.toml b/crates/mun_test/Cargo.toml index 3ec92c20a..1711d66eb 100644 --- a/crates/mun_test/Cargo.toml +++ b/crates/mun_test/Cargo.toml @@ -17,3 +17,4 @@ anyhow = "1.0" compiler = { path = "../mun_compiler", package = "mun_compiler" } runtime = { path = "../mun_runtime", package = "mun_runtime" } tempfile = "3" +itertools = "0.9.0" diff --git a/crates/mun_test/src/fixture.rs b/crates/mun_test/src/fixture.rs new file mode 100644 index 000000000..b2ad41de1 --- /dev/null +++ b/crates/mun_test/src/fixture.rs @@ -0,0 +1,221 @@ +use compiler::RelativePathBuf; +use itertools::Itertools; + +const DEFAULT_FILE_NAME: &str = "main.mun"; +const META_LINE: &str = "//-"; + +/// A `Fixture` describes an single file in a project workspace. `Fixture`s can be parsed from a +/// single string with the `parse` function. Using that function enables users to conveniently +/// describe an entire workspace in a single string. +#[derive(Debug, Eq, PartialEq)] +pub struct Fixture { + /// The relative path of this file + pub relative_path: RelativePathBuf, + + /// The text of the file + pub text: String, +} + +impl Fixture { + /// Parses text which looks like this: + /// + /// ```not_rust + /// //- /foo.mun + /// fn hello_world() { + /// } + /// + /// //- /bar.mun + /// fn baz() { + /// } + /// ``` + /// + /// into two separate `Fixture`s one with `relative_path` 'foo.mun' and one with 'bar.mun'. + pub fn parse(text: impl AsRef) -> Vec { + let text = trim_raw_string_literal(text); + let mut result: Vec = Vec::new(); + + // If the text does not contain any meta tags, insert a default meta tag at the start. + let default_start = if text.contains(META_LINE) { + None + } else { + Some(format!("{} /{}", META_LINE, DEFAULT_FILE_NAME)) + }; + + for (idx, line) in default_start + .as_deref() + .into_iter() + .chain(text.lines()) + .enumerate() + { + if line.contains(META_LINE) { + assert!( + line.starts_with(META_LINE), + "Metadata line {} has invalid indentation. \ + All metadata lines need to have the same indentation \n\ + The offending line: {:?}", + idx, + line + ); + } + + if line.starts_with(META_LINE) { + let meta = Fixture::parse_meta_line(line); + result.push(meta); + } else if let Some(entry) = result.last_mut() { + entry.text.push_str(line); + entry.text.push_str("\n"); + } + } + + result + } + + /// Parses a fixture meta line like: + /// ``` + /// //- /main.mun + /// ``` + fn parse_meta_line(line: impl AsRef) -> Fixture { + let line = line.as_ref(); + assert!(line.starts_with(META_LINE)); + + let line = line[META_LINE.len()..].trim(); + let components = line.split_ascii_whitespace().collect::>(); + + let path = components[0].to_string(); + assert!(path.starts_with('/')); + let relative_path = RelativePathBuf::from(&path[1..]); + + Fixture { + relative_path, + text: String::new(), + } + } +} + +/// Turns a string that is likely to come from a raw string literal into something that is +/// probably intended. +/// +/// * Strips the first newline if there is one +/// * Removes any initial indentation +/// +/// Example usecase: +/// +/// ``` +/// # fn do_something(s: &str) {} +/// do_something(r#" +/// fn func() { +/// // code +/// } +/// "#) +/// ``` +/// +/// Results in the string (with no leading newline): +/// ```not_rust +/// fn func() { +/// // code +/// } +/// ``` +pub fn trim_raw_string_literal(text: impl AsRef) -> String { + let mut text = text.as_ref(); + if text.starts_with('\n') { + text = &text[1..]; + } + + let minimum_indentation = text + .lines() + .filter(|it| !it.trim().is_empty()) + .map(|it| it.len() - it.trim_start().len()) + .min() + .unwrap_or(0); + + text.lines() + .map(|line| { + if line.len() <= minimum_indentation { + line.trim_start_matches(' ') + } else { + &line[minimum_indentation..] + } + }) + .join("\n") +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn trim_raw_string_literal() { + assert_eq!( + &super::trim_raw_string_literal( + r#" + fn hello_world() { + // code + } + "# + ), + "fn hello_world() {\n // code\n}\n" + ); + } + + #[test] + fn empty_fixture() { + assert_eq!( + Fixture::parse(""), + vec![Fixture { + relative_path: RelativePathBuf::from(DEFAULT_FILE_NAME), + text: "".to_owned() + }] + ); + } + + #[test] + fn single_fixture() { + assert_eq!( + Fixture::parse(format!("{} /foo.mun\nfn hello_world() {{}}", META_LINE)), + vec![Fixture { + relative_path: RelativePathBuf::from("foo.mun"), + text: "fn hello_world() {}\n".to_owned() + }] + ); + } + + #[test] + fn multiple_fixtures() { + assert_eq!( + Fixture::parse( + r#" + //- /foo.mun + fn hello_world() { + } + + //- /bar.mun + fn baz() { + } + "# + ), + vec![ + Fixture { + relative_path: RelativePathBuf::from("foo.mun"), + text: "fn hello_world() {\n}\n\n".to_owned() + }, + Fixture { + relative_path: RelativePathBuf::from("bar.mun"), + text: "fn baz() {\n}\n".to_owned() + } + ] + ); + } + + #[test] + #[should_panic] + fn incorrectly_indented_fixture() { + Fixture::parse( + r" + //- /foo.mun + fn foo() {} + //- /bar.mun + pub fn baz() {} + ", + ); + } +} diff --git a/crates/mun_test/src/lib.rs b/crates/mun_test/src/lib.rs index ed5b2a772..17b9d3cf4 100644 --- a/crates/mun_test/src/lib.rs +++ b/crates/mun_test/src/lib.rs @@ -4,5 +4,7 @@ #![warn(missing_docs)] mod driver; +mod fixture; pub use driver::*; +pub use fixture::{trim_raw_string_literal, Fixture};