1use std::fs;
11use std::io;
12use std::path::{Path, PathBuf};
13
14enum Entry {
16 File(PathBuf, Vec<u8>),
17 Dir(PathBuf),
18 #[cfg(unix)]
19 Symlink {
20 link: PathBuf,
21 target: PathBuf,
22 },
23}
24
25pub struct FileTree {
41 root: PathBuf,
42 entries: Vec<Entry>,
43}
44
45impl FileTree {
46 pub fn new(root: impl Into<PathBuf>) -> Self {
49 Self {
50 root: root.into(),
51 entries: Vec::new(),
52 }
53 }
54
55 pub fn file(mut self, relative_path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
57 self.entries.push(Entry::File(
58 relative_path.into(),
59 contents.into().into_bytes(),
60 ));
61 self
62 }
63
64 pub fn bytes(
66 mut self,
67 relative_path: impl Into<PathBuf>,
68 contents: impl Into<Vec<u8>>,
69 ) -> Self {
70 self.entries
71 .push(Entry::File(relative_path.into(), contents.into()));
72 self
73 }
74
75 pub fn dir(mut self, relative_path: impl Into<PathBuf>) -> Self {
77 self.entries.push(Entry::Dir(relative_path.into()));
78 self
79 }
80
81 #[cfg_attr(not(unix), allow(unused_mut, unused_variables))]
86 pub fn symlink(mut self, link: impl Into<PathBuf>, target: impl Into<PathBuf>) -> Self {
87 #[cfg(unix)]
88 {
89 self.entries.push(Entry::Symlink {
90 link: link.into(),
91 target: target.into(),
92 });
93 }
94 self
96 }
97
98 pub fn build(self) -> io::Result<()> {
100 for e in &self.entries {
101 match e {
102 Entry::File(rel, bytes) => {
103 let target = self.root.join(rel);
104 if let Some(parent) = target.parent() {
105 fs::create_dir_all(parent)?;
106 }
107 fs::write(&target, bytes)?;
108 }
109 Entry::Dir(rel) => {
110 fs::create_dir_all(self.root.join(rel))?;
111 }
112 #[cfg(unix)]
113 Entry::Symlink { link, target } => {
114 let link_path = self.root.join(link);
115 if let Some(parent) = link_path.parent() {
116 fs::create_dir_all(parent)?;
117 }
118 std::os::unix::fs::symlink(target, &link_path)?;
119 }
120 }
121 }
122 Ok(())
123 }
124}
125
126pub fn rust_crate(root: &Path, name: &str, version: &str) -> io::Result<()> {
141 let cargo = format!(
142 "[package]\nname = \"{}\"\nversion = \"{}\"\nedition = \"2021\"\n\n[lib]\npath = \"src/lib.rs\"\n",
143 name, version
144 );
145 FileTree::new(root)
146 .file("Cargo.toml", cargo)
147 .file("src/lib.rs", "//! Sample crate.\n")
148 .build()
149}
150
151pub fn rust_workspace(root: &Path, members: &[&str]) -> io::Result<()> {
167 let members_lines: String = members
168 .iter()
169 .map(|m| format!(" \"{}\",\n", m))
170 .collect();
171 let workspace_toml = format!(
172 "[workspace]\nresolver = \"2\"\nmembers = [\n{}]\n",
173 members_lines
174 );
175 FileTree::new(root)
176 .file("Cargo.toml", workspace_toml)
177 .build()?;
178 for m in members {
179 rust_crate(&root.join(m), m, "0.0.0")?;
180 }
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn build_basic_tree() {
190 let dir = tempfile::tempdir().unwrap();
191 FileTree::new(dir.path())
192 .file("a.txt", "hello")
193 .dir("empty")
194 .file("nested/b.txt", "world")
195 .build()
196 .unwrap();
197 assert!(dir.path().join("a.txt").exists());
198 assert!(dir.path().join("empty").is_dir());
199 assert_eq!(
200 fs::read_to_string(dir.path().join("nested/b.txt")).unwrap(),
201 "world"
202 );
203 }
204
205 #[test]
206 fn rust_crate_layout() {
207 let dir = tempfile::tempdir().unwrap();
208 rust_crate(dir.path(), "sample", "0.1.0").unwrap();
209 let cargo = fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
210 assert!(cargo.contains("name = \"sample\""));
211 assert!(cargo.contains("version = \"0.1.0\""));
212 assert!(dir.path().join("src/lib.rs").exists());
213 }
214
215 #[test]
216 fn rust_workspace_layout() {
217 let dir = tempfile::tempdir().unwrap();
218 rust_workspace(dir.path(), &["alpha", "beta"]).unwrap();
219 let ws = fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
220 assert!(ws.contains("\"alpha\""));
221 assert!(ws.contains("\"beta\""));
222 assert!(dir.path().join("alpha/Cargo.toml").exists());
223 assert!(dir.path().join("beta/src/lib.rs").exists());
224 }
225
226 #[cfg(unix)]
227 #[test]
228 fn symlink_unix() {
229 let dir = tempfile::tempdir().unwrap();
230 FileTree::new(dir.path())
231 .file("real.txt", "data")
232 .symlink("link.txt", "real.txt")
233 .build()
234 .unwrap();
235 assert!(dir.path().join("link.txt").exists());
236 }
237
238 #[cfg(windows)]
239 #[test]
240 fn symlink_no_op_on_windows() {
241 let dir = tempfile::tempdir().unwrap();
242 FileTree::new(dir.path())
244 .symlink("link.txt", "real.txt")
245 .build()
246 .unwrap();
247 assert!(!dir.path().join("link.txt").exists());
248 }
249}