Skip to main content

scute_test_utils/
project.rs

1use tempfile::TempDir;
2
3enum ProjectKind {
4    Empty,
5    Cargo,
6}
7
8/// A throwaway project directory for integration tests.
9///
10/// Use [`cargo()`](Self::cargo) or [`empty()`](Self::empty) to start, chain
11/// builder methods, then call [`build()`](Self::build) to materialize the
12/// directory. The returned [`TempDir`] is cleaned up on drop.
13pub struct TestProject {
14    kind: ProjectKind,
15    dependencies: Vec<(String, String)>,
16    dev_dependencies: Vec<(String, String)>,
17    members: Vec<(String, TestMember)>,
18    source_files: Vec<(String, String)>,
19    scute_config: Option<String>,
20}
21
22/// A workspace member inside a [`TestProject`]. Created via [`TestProject::member`].
23pub struct TestMember {
24    dependencies: Vec<(String, String)>,
25    dev_dependencies: Vec<(String, String)>,
26}
27
28impl TestMember {
29    pub fn dependency(mut self, name: &str, version: &str) -> Self {
30        self.dependencies.push((name.into(), version.into()));
31        self
32    }
33
34    pub fn dev_dependency(mut self, name: &str, version: &str) -> Self {
35        self.dev_dependencies.push((name.into(), version.into()));
36        self
37    }
38}
39
40impl TestProject {
41    /// A bare directory with no project scaffolding.
42    pub fn empty() -> Self {
43        Self {
44            kind: ProjectKind::Empty,
45            dependencies: Vec::new(),
46            dev_dependencies: Vec::new(),
47            members: Vec::new(),
48            source_files: Vec::new(),
49            scute_config: None,
50        }
51    }
52
53    /// A minimal Cargo project with `Cargo.toml` and empty `src/lib.rs`.
54    pub fn cargo() -> Self {
55        Self {
56            kind: ProjectKind::Cargo,
57            dependencies: Vec::new(),
58            dev_dependencies: Vec::new(),
59            members: Vec::new(),
60            source_files: Vec::new(),
61            scute_config: None,
62        }
63    }
64
65    pub fn dependency(mut self, name: &str, version: &str) -> Self {
66        self.dependencies.push((name.into(), version.into()));
67        self
68    }
69
70    pub fn dev_dependency(mut self, name: &str, version: &str) -> Self {
71        self.dev_dependencies.push((name.into(), version.into()));
72        self
73    }
74
75    /// Add a workspace member. The closure receives an empty [`TestMember`]
76    /// to configure with its own dependencies.
77    pub fn member(mut self, name: &str, build: impl FnOnce(TestMember) -> TestMember) -> Self {
78        let member = build(TestMember {
79            dependencies: Vec::new(),
80            dev_dependencies: Vec::new(),
81        });
82        self.members.push((name.into(), member));
83        self
84    }
85
86    pub fn source_file(mut self, name: &str, content: &str) -> Self {
87        self.source_files.push((name.into(), content.into()));
88        self
89    }
90
91    pub fn scute_config(mut self, yaml: &str) -> Self {
92        self.scute_config = Some(yaml.into());
93        self
94    }
95
96    /// Materialize the project into a temporary directory.
97    pub fn build(self) -> TempDir {
98        let dir = TempDir::new().unwrap();
99        if matches!(self.kind, ProjectKind::Cargo) {
100            setup_cargo_project(
101                &dir,
102                &self.dependencies,
103                &self.dev_dependencies,
104                &self.members,
105            );
106        }
107        for (name, content) in &self.source_files {
108            let path = dir.path().join(name);
109            if let Some(parent) = path.parent() {
110                std::fs::create_dir_all(parent).unwrap();
111            }
112            std::fs::write(path, content).unwrap();
113        }
114        if let Some(yaml) = &self.scute_config {
115            std::fs::write(dir.path().join(".scute.yml"), yaml).unwrap();
116        }
117        dir
118    }
119}
120
121fn setup_cargo_project(
122    dir: &TempDir,
123    dependencies: &[(String, String)],
124    dev_dependencies: &[(String, String)],
125    members: &[(String, TestMember)],
126) {
127    let mut toml = if members.is_empty() {
128        String::from(
129            "[package]\nname = \"test-project\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
130        )
131    } else {
132        let names: Vec<&str> = members.iter().map(|(n, _)| n.as_str()).collect();
133        format!(
134            "[workspace]\nmembers = [{}]\n",
135            names
136                .iter()
137                .map(|n| format!("\"{n}\""))
138                .collect::<Vec<_>>()
139                .join(", ")
140        )
141    };
142
143    append_cargo_deps(&mut toml, dependencies, dev_dependencies);
144
145    std::fs::write(dir.path().join("Cargo.toml"), toml).unwrap();
146
147    if members.is_empty() {
148        let src = dir.path().join("src");
149        std::fs::create_dir(&src).unwrap();
150        std::fs::write(src.join("lib.rs"), "").unwrap();
151    }
152
153    for (name, member) in members {
154        setup_cargo_member(dir, name, &member.dependencies, &member.dev_dependencies);
155    }
156}
157
158fn setup_cargo_member(
159    dir: &TempDir,
160    name: &str,
161    dependencies: &[(String, String)],
162    dev_dependencies: &[(String, String)],
163) {
164    let member_dir = dir.path().join(name);
165    std::fs::create_dir_all(member_dir.join("src")).unwrap();
166    std::fs::write(member_dir.join("src/lib.rs"), "").unwrap();
167
168    let mut toml =
169        format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n");
170    append_cargo_deps(&mut toml, dependencies, dev_dependencies);
171    std::fs::write(member_dir.join("Cargo.toml"), toml).unwrap();
172}
173
174fn append_cargo_deps(
175    toml: &mut String,
176    dependencies: &[(String, String)],
177    dev_dependencies: &[(String, String)],
178) {
179    use std::fmt::Write;
180
181    for (section, deps) in [
182        ("[dependencies]", dependencies),
183        ("[dev-dependencies]", dev_dependencies),
184    ] {
185        if !deps.is_empty() {
186            writeln!(toml, "\n{section}").unwrap();
187            for (name, version) in deps {
188                writeln!(toml, "{name} = \"{version}\"").unwrap();
189            }
190        }
191    }
192}