Skip to main content

turtle_build/
load.rs

1use crate::{
2    ast::{Module, Statement},
3    error::BuildError,
4    file::canonicalize_native_path,
5    infrastructure::FileSystem,
6    parse::parse,
7};
8use std::{
9    collections::HashMap,
10    path::{Path, PathBuf},
11};
12
13/// Loads a root module and its dependency modules.
14pub async fn load(
15    file_system: &impl FileSystem,
16    path: &Path,
17) -> Result<HashMap<PathBuf, Module>, BuildError> {
18    let mut paths = vec![(path.to_path_buf(), vec![])];
19    let mut modules = HashMap::new();
20
21    while let Some((path, mut ancestors)) = paths.pop() {
22        let canonical_path = canonicalize_native_path(&path);
23
24        if ancestors.contains(&canonical_path) {
25            return Err(BuildError::CircularModuleDependency);
26        } else if modules.contains_key(&canonical_path) {
27            continue;
28        }
29
30        let module = parse(&file_system.read_file_to_string(&path).await?)?;
31
32        ancestors.push(canonical_path.clone());
33        paths.extend(
34            module
35                .statements()
36                .iter()
37                .filter_map(|statement| match statement {
38                    Statement::Include(include) => Some(include.path()),
39                    Statement::Submodule(submodule) => Some(submodule.path()),
40                    _ => None,
41                })
42                // TODO Interpolate variables in paths of included and subninja files like ninja.
43                .map(|path| (path.into(), ancestors.clone())),
44        );
45
46        modules.insert(canonical_path, module);
47    }
48
49    Ok(modules)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::infrastructure::{FakeFileSystem, FileError};
56    use pretty_assertions::assert_eq;
57
58    fn create_modules(sources: &[(&str, &str)]) -> HashMap<PathBuf, Module> {
59        sources
60            .iter()
61            .map(|(path, source)| (PathBuf::from(path), parse(source).unwrap()))
62            .collect()
63    }
64
65    fn create_file_system(sources: &[(&str, &str)]) -> FakeFileSystem {
66        let file_system = FakeFileSystem::default();
67
68        for (path, source) in sources {
69            file_system.write_file(path, source);
70        }
71
72        file_system
73    }
74
75    #[tokio::test]
76    async fn load_root_module() {
77        let sources = [("build.ninja", "x = 42\n")];
78
79        assert_eq!(
80            load(&create_file_system(&sources), Path::new("build.ninja")).await,
81            Ok(create_modules(&sources))
82        );
83    }
84
85    #[tokio::test]
86    async fn load_included_module() {
87        let sources = [
88            ("build.ninja", "include foo.ninja\n"),
89            ("foo.ninja", "x = 42\n"),
90        ];
91
92        assert_eq!(
93            load(&create_file_system(&sources), Path::new("build.ninja")).await,
94            Ok(create_modules(&sources))
95        );
96    }
97
98    #[tokio::test]
99    async fn load_child_module() {
100        let sources = [
101            ("build.ninja", "subninja foo.ninja\n"),
102            ("foo.ninja", "x = 42\n"),
103        ];
104
105        assert_eq!(
106            load(&create_file_system(&sources), Path::new("build.ninja")).await,
107            Ok(create_modules(&sources))
108        );
109    }
110
111    #[tokio::test]
112    async fn load_module_included_from_two_modules() {
113        let sources = [
114            ("build.ninja", "include foo.ninja\ninclude bar.ninja\n"),
115            ("foo.ninja", "include baz.ninja\n"),
116            ("bar.ninja", "subninja baz.ninja\n"),
117            ("baz.ninja", "x = 42\n"),
118        ];
119        let file_system = create_file_system(&sources);
120
121        assert_eq!(
122            load(&file_system, Path::new("build.ninja")).await,
123            Ok(create_modules(&sources))
124        );
125        assert_eq!(
126            file_system
127                .read_requests()
128                .iter()
129                .filter(|path| path == &Path::new("baz.ninja"))
130                .count(),
131            1
132        );
133    }
134
135    #[tokio::test]
136    async fn canonicalize_root_module_path() {
137        let source = "x = 42\n";
138        let file_system = create_file_system(&[("./build.ninja", source)]);
139
140        assert_eq!(
141            load(&file_system, Path::new("./build.ninja")).await,
142            Ok(create_modules(&[("build.ninja", source)]))
143        );
144        assert_eq!(
145            file_system.read_requests(),
146            [PathBuf::from("./build.ninja")]
147        );
148    }
149
150    #[tokio::test]
151    async fn canonicalize_included_module_paths() {
152        let root_source = "include ./foo.ninja\nsubninja bar/../foo.ninja\n";
153        let source = "x = 42\n";
154        let file_system = create_file_system(&[
155            ("build.ninja", root_source),
156            ("./foo.ninja", source),
157            ("bar/../foo.ninja", source),
158        ]);
159
160        assert_eq!(
161            load(&file_system, Path::new("build.ninja")).await,
162            Ok(create_modules(&[
163                ("build.ninja", root_source),
164                ("foo.ninja", source)
165            ]))
166        );
167        assert_eq!(file_system.read_requests().len(), 2);
168    }
169
170    #[tokio::test]
171    async fn resolve_module_path_relative_to_working_directory() {
172        let sources = [
173            ("build.ninja", "include foo/foo.ninja\n"),
174            ("foo/foo.ninja", "include bar.ninja\n"),
175            ("bar.ninja", "x = 42\n"),
176        ];
177
178        assert_eq!(
179            load(&create_file_system(&sources), Path::new("build.ninja")).await,
180            Ok(create_modules(&sources))
181        );
182    }
183
184    #[tokio::test]
185    async fn fail_to_read_module() {
186        assert_eq!(
187            load(
188                &create_file_system(&[("build.ninja", "include foo.ninja\n")]),
189                Path::new("build.ninja")
190            )
191            .await,
192            Err(FileError::new("file not found").into())
193        );
194    }
195
196    #[tokio::test]
197    async fn fail_to_read_module_in_missing_directory() {
198        assert_eq!(
199            load(
200                &create_file_system(&[
201                    ("build.ninja", "include bar/../foo.ninja\n"),
202                    ("foo.ninja", "x = 42\n"),
203                ]),
204                Path::new("build.ninja")
205            )
206            .await,
207            Err(FileError::new("file not found").into())
208        );
209    }
210
211    #[tokio::test]
212    async fn fail_to_include_module_in_itself() {
213        assert_eq!(
214            load(
215                &create_file_system(&[("build.ninja", "include build.ninja\n")]),
216                Path::new("build.ninja")
217            )
218            .await,
219            Err(BuildError::CircularModuleDependency)
220        );
221    }
222
223    #[tokio::test]
224    async fn fail_to_include_modules_in_each_other() {
225        assert_eq!(
226            load(
227                &create_file_system(&[
228                    ("build.ninja", "subninja foo.ninja\n"),
229                    ("foo.ninja", "subninja ./build.ninja\n"),
230                ]),
231                Path::new("build.ninja")
232            )
233            .await,
234            Err(BuildError::CircularModuleDependency)
235        );
236    }
237}