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 write_source_files(root, &self.source_files);
154 if let Some(yaml) = &self.scute_config {
155 std::fs::write(root.join(".scute.yml"), yaml).unwrap();
156 }
157 for (path, child) in self.children {
158 child.setup_at(&root.join(path));
159 }
160 }
161}
162
163fn setup_cargo_project(
164 root: &Path,
165 dependencies: &[(String, String)],
166 dev_dependencies: &[(String, String)],
167 members: &[(String, TestMember)],
168) {
169 let mut toml = if members.is_empty() {
170 String::from(
171 "[package]\nname = \"test-project\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
172 )
173 } else {
174 format!(
175 "[workspace]\nmembers = [{}]\n",
176 members
177 .iter()
178 .map(|(path, _)| format!("\"{path}\""))
179 .collect::<Vec<_>>()
180 .join(", ")
181 )
182 };
183
184 append_cargo_deps(&mut toml, dependencies, dev_dependencies);
185
186 std::fs::write(root.join("Cargo.toml"), toml).unwrap();
187
188 if members.is_empty() {
189 let src = root.join("src");
190 std::fs::create_dir(&src).unwrap();
191 std::fs::write(src.join("lib.rs"), "").unwrap();
192 }
193
194 for (path, member) in members {
195 setup_cargo_member(root, path, member);
196 }
197}
198
199fn setup_cargo_member(root: &Path, path: &str, member: &TestMember) {
200 let member_dir = root.join(path);
201 std::fs::create_dir_all(member_dir.join("src")).unwrap();
202 std::fs::write(member_dir.join("src/lib.rs"), "").unwrap();
203
204 let name = Path::new(path).file_name().unwrap().to_string_lossy();
205 let mut toml =
206 format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n");
207 append_cargo_deps(&mut toml, &member.dependencies, &member.dev_dependencies);
208 std::fs::write(member_dir.join("Cargo.toml"), toml).unwrap();
209}
210
211enum JsToolchain {
212 Npm,
213 Pnpm,
214}
215
216impl JsToolchain {
217 fn command(&self) -> &str {
218 match self {
219 Self::Npm => "npm",
220 Self::Pnpm => "pnpm",
221 }
222 }
223}
224
225fn setup_js_project(
226 root: &Path,
227 dependencies: &[(String, String)],
228 dev_dependencies: &[(String, String)],
229 members: &[(String, TestMember)],
230 toolchain: &JsToolchain,
231) {
232 let mut pkg = serde_json::Map::new();
233 pkg.insert("name".into(), "test-project".into());
234 pkg.insert("version".into(), "1.0.0".into());
235
236 if !members.is_empty() {
237 match toolchain {
238 JsToolchain::Npm => {
239 let workspace_paths: Vec<serde_json::Value> = members
240 .iter()
241 .map(|(path, _)| serde_json::Value::String(path.clone()))
242 .collect();
243 pkg.insert("workspaces".into(), workspace_paths.into());
244 }
245 JsToolchain::Pnpm => {
246 let yaml = members
247 .iter()
248 .map(|(path, _)| format!(" - {path}"))
249 .collect::<Vec<_>>()
250 .join("\n");
251 std::fs::write(
252 root.join("pnpm-workspace.yaml"),
253 format!("packages:\n{yaml}"),
254 )
255 .unwrap();
256 }
257 }
258 }
259
260 append_js_deps(&mut pkg, dependencies, dev_dependencies);
261
262 let json = serde_json::to_string_pretty(&pkg).unwrap();
263 std::fs::write(root.join("package.json"), json).unwrap();
264
265 for (path, member) in members {
266 setup_js_member(root, path, member);
267 }
268
269 let cmd = toolchain.command();
270 let output = std::process::Command::new(cmd)
271 .args(["install"])
272 .current_dir(root)
273 .output()
274 .unwrap_or_else(|_| panic!("{cmd} must be installed to run {cmd} integration tests"));
275
276 assert!(
277 output.status.success(),
278 "{cmd} install failed: {}",
279 String::from_utf8_lossy(&output.stderr)
280 );
281}
282
283fn setup_js_member(root: &Path, path: &str, member: &TestMember) {
284 let member_dir = root.join(path);
285 std::fs::create_dir_all(&member_dir).unwrap();
286
287 let basename = Path::new(path).file_name().unwrap().to_string_lossy();
288
289 let mut pkg = serde_json::Map::new();
290 pkg.insert("name".into(), format!("@test/{basename}").into());
291 pkg.insert("version".into(), "1.0.0".into());
292
293 append_js_deps(&mut pkg, &member.dependencies, &member.dev_dependencies);
294
295 let json = serde_json::to_string_pretty(&pkg).unwrap();
296 std::fs::write(member_dir.join("package.json"), json).unwrap();
297}
298
299fn append_js_deps(
300 pkg: &mut serde_json::Map<String, serde_json::Value>,
301 dependencies: &[(String, String)],
302 dev_dependencies: &[(String, String)],
303) {
304 for (key, deps) in [
305 ("dependencies", dependencies),
306 ("devDependencies", dev_dependencies),
307 ] {
308 if !deps.is_empty() {
309 let map: serde_json::Map<String, serde_json::Value> = deps
310 .iter()
311 .map(|(n, v)| (n.clone(), serde_json::Value::String(v.clone())))
312 .collect();
313 pkg.insert(key.into(), map.into());
314 }
315 }
316}
317
318fn write_source_files(root: &Path, files: &[(String, String)]) {
319 for (name, content) in files {
320 let path = root.join(name);
321 if let Some(parent) = path.parent() {
322 std::fs::create_dir_all(parent).unwrap();
323 }
324 std::fs::write(path, content).unwrap();
325 }
326}
327
328fn init_git_repo(root: &Path) {
329 std::fs::create_dir(root.join(".git")).unwrap();
330 std::fs::write(root.join(".gitignore"), "node_modules/\ntarget/\n").unwrap();
331}
332
333fn append_cargo_deps(
334 toml: &mut String,
335 dependencies: &[(String, String)],
336 dev_dependencies: &[(String, String)],
337) {
338 use std::fmt::Write;
339
340 for (section, deps) in [
341 ("[dependencies]", dependencies),
342 ("[dev-dependencies]", dev_dependencies),
343 ]
344 .into_iter()
345 .filter(|(_, deps)| !deps.is_empty())
346 {
347 writeln!(toml, "\n{section}").unwrap();
348 for (name, version) in deps {
349 writeln!(toml, "{name} = \"{version}\"").unwrap();
350 }
351 }
352}