1use std::path::Path;
2
3use tempfile::TempDir;
4
5enum ProjectKind {
6 Empty,
7 Cargo,
8 Npm,
9 Pnpm,
10}
11
12pub struct TestProject {
19 kind: ProjectKind,
20 dependencies: Vec<(String, String)>,
21 dev_dependencies: Vec<(String, String)>,
22 members: Vec<(String, TestMember)>,
23 children: Vec<(String, TestProject)>,
24 source_files: Vec<(String, String)>,
25 scute_config: Option<String>,
26}
27
28pub struct TestMember {
30 dependencies: Vec<(String, String)>,
31 dev_dependencies: Vec<(String, String)>,
32}
33
34impl TestMember {
35 pub fn dependency(mut self, name: &str, version: &str) -> Self {
36 self.dependencies.push((name.into(), version.into()));
37 self
38 }
39
40 pub fn dev_dependency(mut self, name: &str, version: &str) -> Self {
41 self.dev_dependencies.push((name.into(), version.into()));
42 self
43 }
44}
45
46impl TestProject {
47 fn new(kind: ProjectKind) -> Self {
48 Self {
49 kind,
50 dependencies: Vec::new(),
51 dev_dependencies: Vec::new(),
52 members: Vec::new(),
53 children: Vec::new(),
54 source_files: Vec::new(),
55 scute_config: None,
56 }
57 }
58
59 pub fn empty() -> Self {
61 Self::new(ProjectKind::Empty)
62 }
63
64 pub fn npm() -> Self {
66 Self::new(ProjectKind::Npm)
67 }
68
69 pub fn pnpm() -> Self {
71 Self::new(ProjectKind::Pnpm)
72 }
73
74 pub fn cargo() -> Self {
76 Self::new(ProjectKind::Cargo)
77 }
78
79 pub fn dependency(mut self, name: &str, version: &str) -> Self {
80 self.dependencies.push((name.into(), version.into()));
81 self
82 }
83
84 pub fn dev_dependency(mut self, name: &str, version: &str) -> Self {
85 self.dev_dependencies.push((name.into(), version.into()));
86 self
87 }
88
89 pub fn member(mut self, path: &str, build: impl FnOnce(TestMember) -> TestMember) -> Self {
92 let member = build(TestMember {
93 dependencies: Vec::new(),
94 dev_dependencies: Vec::new(),
95 });
96 self.members.push((path.into(), member));
97 self
98 }
99
100 pub fn source_file(mut self, name: &str, content: &str) -> Self {
101 self.source_files.push((name.into(), content.into()));
102 self
103 }
104
105 pub fn scute_config(mut self, yaml: &str) -> Self {
106 self.scute_config = Some(yaml.into());
107 self
108 }
109
110 pub fn nested(mut self, path: &str, child: TestProject) -> Self {
112 self.children.push((path.into(), child));
113 self
114 }
115
116 pub fn build(self) -> TempDir {
122 let dir = TempDir::new().unwrap();
123 init_git_repo(dir.path());
124 self.setup_at(dir.path());
125 dir
126 }
127
128 fn setup_at(self, root: &Path) {
129 std::fs::create_dir_all(root).unwrap();
130 match self.kind {
131 ProjectKind::Cargo => setup_cargo_project(
132 root,
133 &self.dependencies,
134 &self.dev_dependencies,
135 &self.members,
136 ),
137 ProjectKind::Npm => setup_js_project(
138 root,
139 &self.dependencies,
140 &self.dev_dependencies,
141 &self.members,
142 &JsToolchain::Npm,
143 ),
144 ProjectKind::Pnpm => setup_js_project(
145 root,
146 &self.dependencies,
147 &self.dev_dependencies,
148 &self.members,
149 &JsToolchain::Pnpm,
150 ),
151 ProjectKind::Empty => {}
152 }
153 for (name, content) in &self.source_files {
154 let path = root.join(name);
155 if let Some(parent) = path.parent() {
156 std::fs::create_dir_all(parent).unwrap();
157 }
158 std::fs::write(path, content).unwrap();
159 }
160 if let Some(yaml) = &self.scute_config {
161 std::fs::write(root.join(".scute.yml"), yaml).unwrap();
162 }
163 for (path, child) in self.children {
164 child.setup_at(&root.join(path));
165 }
166 }
167}
168
169fn setup_cargo_project(
170 root: &Path,
171 dependencies: &[(String, String)],
172 dev_dependencies: &[(String, String)],
173 members: &[(String, TestMember)],
174) {
175 let mut toml = if members.is_empty() {
176 String::from(
177 "[package]\nname = \"test-project\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
178 )
179 } else {
180 format!(
181 "[workspace]\nmembers = [{}]\n",
182 members
183 .iter()
184 .map(|(path, _)| format!("\"{path}\""))
185 .collect::<Vec<_>>()
186 .join(", ")
187 )
188 };
189
190 append_cargo_deps(&mut toml, dependencies, dev_dependencies);
191
192 std::fs::write(root.join("Cargo.toml"), toml).unwrap();
193
194 if members.is_empty() {
195 let src = root.join("src");
196 std::fs::create_dir(&src).unwrap();
197 std::fs::write(src.join("lib.rs"), "").unwrap();
198 }
199
200 for (path, member) in members {
201 setup_cargo_member(root, path, member);
202 }
203}
204
205fn setup_cargo_member(root: &Path, path: &str, member: &TestMember) {
206 let member_dir = root.join(path);
207 std::fs::create_dir_all(member_dir.join("src")).unwrap();
208 std::fs::write(member_dir.join("src/lib.rs"), "").unwrap();
209
210 let name = Path::new(path).file_name().unwrap().to_string_lossy();
211 let mut toml =
212 format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n");
213 append_cargo_deps(&mut toml, &member.dependencies, &member.dev_dependencies);
214 std::fs::write(member_dir.join("Cargo.toml"), toml).unwrap();
215}
216
217enum JsToolchain {
218 Npm,
219 Pnpm,
220}
221
222impl JsToolchain {
223 fn command(&self) -> &str {
224 match self {
225 Self::Npm => "npm",
226 Self::Pnpm => "pnpm",
227 }
228 }
229}
230
231fn setup_js_project(
232 root: &Path,
233 dependencies: &[(String, String)],
234 dev_dependencies: &[(String, String)],
235 members: &[(String, TestMember)],
236 toolchain: &JsToolchain,
237) {
238 let mut pkg = serde_json::Map::new();
239 pkg.insert("name".into(), "test-project".into());
240 pkg.insert("version".into(), "1.0.0".into());
241
242 if !members.is_empty() {
243 match toolchain {
244 JsToolchain::Npm => {
245 let workspace_paths: Vec<serde_json::Value> = members
246 .iter()
247 .map(|(path, _)| serde_json::Value::String(path.clone()))
248 .collect();
249 pkg.insert("workspaces".into(), workspace_paths.into());
250 }
251 JsToolchain::Pnpm => {
252 let yaml = members
253 .iter()
254 .map(|(path, _)| format!(" - {path}"))
255 .collect::<Vec<_>>()
256 .join("\n");
257 std::fs::write(
258 root.join("pnpm-workspace.yaml"),
259 format!("packages:\n{yaml}"),
260 )
261 .unwrap();
262 }
263 }
264 }
265
266 append_js_deps(&mut pkg, dependencies, dev_dependencies);
267
268 let json = serde_json::to_string_pretty(&pkg).unwrap();
269 std::fs::write(root.join("package.json"), json).unwrap();
270
271 for (path, member) in members {
272 setup_js_member(root, path, member);
273 }
274
275 let cmd = toolchain.command();
276 let output = std::process::Command::new(cmd)
277 .args(["install"])
278 .current_dir(root)
279 .output()
280 .unwrap_or_else(|_| panic!("{cmd} must be installed to run {cmd} integration tests"));
281
282 assert!(
283 output.status.success(),
284 "{cmd} install failed: {}",
285 String::from_utf8_lossy(&output.stderr)
286 );
287}
288
289fn setup_js_member(root: &Path, path: &str, member: &TestMember) {
290 let member_dir = root.join(path);
291 std::fs::create_dir_all(&member_dir).unwrap();
292
293 let basename = Path::new(path).file_name().unwrap().to_string_lossy();
294
295 let mut pkg = serde_json::Map::new();
296 pkg.insert("name".into(), format!("@test/{basename}").into());
297 pkg.insert("version".into(), "1.0.0".into());
298
299 append_js_deps(&mut pkg, &member.dependencies, &member.dev_dependencies);
300
301 let json = serde_json::to_string_pretty(&pkg).unwrap();
302 std::fs::write(member_dir.join("package.json"), json).unwrap();
303}
304
305fn append_js_deps(
306 pkg: &mut serde_json::Map<String, serde_json::Value>,
307 dependencies: &[(String, String)],
308 dev_dependencies: &[(String, String)],
309) {
310 for (key, deps) in [
311 ("dependencies", dependencies),
312 ("devDependencies", dev_dependencies),
313 ] {
314 if !deps.is_empty() {
315 let map: serde_json::Map<String, serde_json::Value> = deps
316 .iter()
317 .map(|(n, v)| (n.clone(), serde_json::Value::String(v.clone())))
318 .collect();
319 pkg.insert(key.into(), map.into());
320 }
321 }
322}
323
324fn init_git_repo(root: &Path) {
325 std::fs::create_dir(root.join(".git")).unwrap();
326 std::fs::write(root.join(".gitignore"), "node_modules/\ntarget/\n").unwrap();
327}
328
329fn append_cargo_deps(
330 toml: &mut String,
331 dependencies: &[(String, String)],
332 dev_dependencies: &[(String, String)],
333) {
334 use std::fmt::Write;
335
336 for (section, deps) in [
337 ("[dependencies]", dependencies),
338 ("[dev-dependencies]", dev_dependencies),
339 ] {
340 if !deps.is_empty() {
341 writeln!(toml, "\n{section}").unwrap();
342 for (name, version) in deps {
343 writeln!(toml, "{name} = \"{version}\"").unwrap();
344 }
345 }
346 }
347}