1use std::collections::BTreeMap;
19use std::io::Write;
20use std::path::{Path, PathBuf};
21use std::process::{Command, Stdio};
22
23use onevcs::registry::{Checkout, Identity, Registry};
24use onevcs::{testing, Error, Result};
25
26pub const IDENTITIES: usize = 20;
28pub const TERMS_PER_IDENTITY: usize = 100;
31pub const PATHS: usize = 1000;
33pub const BYTES: usize = 10 * 1024 * 1024;
35
36pub const DESTINATION: &str = "github.com/sample-owner/openwidget";
38pub const PUBLICATION_BRANCH: &str = "generic-workload";
40pub const EXPORT_BRANCH: &str = "generic-examples";
42pub const EXPORT_DIRECTORY: &str = "examples";
44
45#[derive(Debug)]
47pub struct Workload {
48 pub root: PathBuf,
50 pub home: PathBuf,
52 pub destination: PathBuf,
54 pub destination_origin: PathBuf,
56 pub source: String,
58 pub identities: Vec<String>,
60 pub bytes: usize,
62}
63
64pub fn identity(index: usize) -> String {
66 format!("github.com/shelterco{index:02}/quietvault{index:02}")
67}
68
69pub fn build(root: &Path) -> Result<Workload> {
71 if root.exists() && std::fs::read_dir(root).map_err(io)?.next().is_some() {
72 return Err(invalid(format!(
73 "workload root {} must be empty",
74 root.display()
75 )));
76 }
77 std::fs::create_dir_all(root).map_err(io)?;
78 let root = std::fs::canonicalize(root).map_err(io)?;
79 let home = root.join("home");
80 std::fs::create_dir_all(&home).map_err(io)?;
81 std::fs::write(
82 root.join(".gitconfig"),
83 "[user]\nname=Fixture\nemail=fixture@example.invalid\n[commit]\ngpgsign=false\n\
84 [maintenance]\nauto=false\n[init]\ndefaultBranch=main\n",
85 )
86 .map_err(io)?;
87 let rules = home.join("rules.yml");
88 std::fs::write(
89 &rules,
90 "version: 4\nrules:\n - match: {host: github.com, owner: \"shelterco*\"}\n \
91 visibility: private\n - match: {host: github.com, owner: sample-owner, name: \
92 openwidget}\n visibility: public\ndefault: {publication: local-direct, approvals: \
93 none}\n",
94 )
95 .map_err(io)?;
96
97 let files = workload_files();
98 let bytes = files.iter().map(|(_, body)| body.len()).sum();
99 let mut registry = Registry {
100 version: testing::registry_version(),
101 identities: BTreeMap::new(),
102 checkouts: BTreeMap::new(),
103 rules: Some(rules),
104 };
105 let mut identities = Vec::new();
106 for index in 0..IDENTITIES {
107 let key = identity(index);
108 let checkout = private_repository(&root, index, &files)?;
109 registry
110 .identities
111 .insert(key.clone(), Identity::new(key.clone(), "<no-op>"));
112 registry.checkouts.insert(
113 format!("quietvault{index:02}"),
114 Checkout {
115 path: checkout,
116 identity: key.clone(),
117 },
118 );
119 identities.push(key);
120 }
121 let (destination, destination_origin) = public_repository(&root, &files)?;
122 registry.identities.insert(
123 DESTINATION.to_owned(),
124 Identity::new(DESTINATION, "<no-op>"),
125 );
126 registry.checkouts.insert(
127 "openwidget".to_owned(),
128 Checkout {
129 path: destination.clone(),
130 identity: DESTINATION.to_owned(),
131 },
132 );
133 testing::write_registry(&home, ®istry)?;
134 Ok(Workload {
135 root,
136 home,
137 destination,
138 destination_origin,
139 source: identity(0),
140 identities,
141 bytes,
142 })
143}
144
145const WORDS: [&str; 32] = [
147 "amber", "basin", "cedar", "delta", "ember", "fable", "grove", "hazel", "inlet", "jasper",
148 "kettle", "lumen", "maple", "nectar", "orbit", "pebble", "quartz", "ripple", "sable",
149 "thistle", "umber", "velvet", "willow", "xenon", "yarrow", "zephyr", "anchor", "bramble",
150 "canyon", "drift", "estuary", "fern",
151];
152
153fn workload_files() -> Vec<(String, String)> {
156 let mut state: u64 = 0x5eed_b0a7;
157 let per_file = BYTES / PATHS;
158 let mut remainder = BYTES % PATHS;
159 (0..PATHS)
160 .map(|index| {
161 let mut size = per_file;
162 if remainder > 0 {
163 size += 1;
164 remainder -= 1;
165 }
166 let mut text = String::with_capacity(size + 64);
167 let mut line = 0;
168 while text.len() < size {
169 line += 1;
170 text.push_str(&format!("line {line}:"));
171 for _ in 0..9 {
172 state = state
173 .wrapping_mul(6_364_136_223_846_793_005)
174 .wrapping_add(1_442_695_040_888_963_407);
175 text.push(' ');
176 text.push_str(WORDS[(state >> 59) as usize]);
177 }
178 text.push('\n');
179 }
180 text.truncate(size - 1);
181 text.push('\n');
182 (format!("part-{:02}/file-{index:04}.txt", index % 25), text)
183 })
184 .collect()
185}
186
187fn private_repository(root: &Path, index: usize, files: &[(String, String)]) -> Result<PathBuf> {
190 let name = format!("quietvault{index:02}");
191 let repo = root.join("checkouts").join(&name);
192 std::fs::create_dir_all(&repo).map_err(io)?;
193 git(&repo, &["init", "-q", "-b", "main"])?;
194 let declared: Vec<String> = (0..TERMS_PER_IDENTITY - 4)
195 .map(|term| format!("\"{name}x{term:03}\""))
196 .collect();
197 let base = vec![
198 ("README.md".to_owned(), "# notes\n".to_owned()),
199 (
200 "Cargo.toml".to_owned(),
201 format!("[package]\nname = \"{name}-core\"\n"),
202 ),
203 (
204 "private-terms.toml".to_owned(),
205 format!("schema_version = 1\nterms = [{}]\n", declared.join(", ")),
206 ),
207 ];
208 let mut import = Import::default();
209 let main = import.commit("main", None, "chore: the repository", &base);
210 if index == 0 {
211 let examples: Vec<(String, String)> = files
212 .iter()
213 .map(|(path, body)| (format!("{EXPORT_DIRECTORY}/{path}"), body.clone()))
214 .collect();
215 import.commit(
216 EXPORT_BRANCH,
217 Some(main),
218 "docs: add generic examples",
219 &examples,
220 );
221 }
222 import.run(&repo)?;
223 git(&repo, &["checkout", "-q", "main"])?;
224 if index == 0 {
225 let origin = root.join("origins").join(format!("{name}.git"));
226 git(
227 root,
228 &[
229 "init",
230 "-q",
231 "--bare",
232 "-b",
233 "main",
234 &origin.to_string_lossy(),
235 ],
236 )?;
237 git(
238 &repo,
239 &["remote", "add", "origin", &origin.to_string_lossy()],
240 )?;
241 git(&repo, &["push", "-q", "origin", "main"])?;
242 git(&repo, &["fetch", "-q", "origin"])?;
243 git(&repo, &["remote", "set-head", "origin", "main"])?;
244 }
245 Ok(repo)
246}
247
248fn public_repository(root: &Path, files: &[(String, String)]) -> Result<(PathBuf, PathBuf)> {
251 let origin = root.join("origins/openwidget.git");
252 let repo = root.join("checkouts/openwidget");
253 std::fs::create_dir_all(&repo).map_err(io)?;
254 git(
255 root,
256 &[
257 "init",
258 "-q",
259 "--bare",
260 "-b",
261 "main",
262 &origin.to_string_lossy(),
263 ],
264 )?;
265 git(&repo, &["init", "-q", "-b", "main"])?;
266 let mut import = Import::default();
267 let main = import.commit(
268 "main",
269 None,
270 "chore: seed the repository",
271 &[("README.md".to_owned(), "# openwidget\n".to_owned())],
272 );
273 let workload: Vec<(String, String)> = files
274 .iter()
275 .map(|(path, body)| (format!("workload/{path}"), body.clone()))
276 .collect();
277 import.commit(
278 PUBLICATION_BRANCH,
279 Some(main),
280 "docs: add generic workload fixtures",
281 &workload,
282 );
283 import.run(&repo)?;
284 git(&repo, &["checkout", "-q", "main"])?;
285 git(
286 &repo,
287 &["remote", "add", "origin", &origin.to_string_lossy()],
288 )?;
289 git(&repo, &["push", "-q", "origin", "main"])?;
290 git(&repo, &["fetch", "-q", "origin"])?;
291 git(&repo, &["remote", "set-head", "origin", "main"])?;
292 git(
293 &repo,
294 &["branch", "-q", "--set-upstream-to=origin/main", "main"],
295 )?;
296 Ok((repo, origin))
297}
298
299#[derive(Default)]
301struct Import {
302 bytes: Vec<u8>,
303 next: usize,
304}
305
306impl Import {
307 fn mark(&mut self) -> usize {
308 self.next += 1;
309 self.next
310 }
311
312 fn commit(
313 &mut self,
314 branch: &str,
315 parent: Option<usize>,
316 subject: &str,
317 files: &[(String, String)],
318 ) -> usize {
319 let mut blobs = Vec::new();
320 for (path, body) in files {
321 let mark = self.mark();
322 write!(self.bytes, "blob\nmark :{mark}\ndata {}\n", body.len()).expect("memory");
323 self.bytes.extend_from_slice(body.as_bytes());
324 self.bytes.push(b'\n');
325 blobs.push((path, mark));
326 }
327 let mark = self.mark();
328 write!(
329 self.bytes,
330 "commit refs/heads/{branch}\nmark :{mark}\ncommitter Fixture \
331 <fixture@example.invalid> 1700000000 +0000\ndata {}\n{subject}\n",
332 subject.len()
333 )
334 .expect("memory");
335 if let Some(parent) = parent {
336 writeln!(self.bytes, "from :{parent}").expect("memory");
337 }
338 for (path, blob) in blobs {
339 writeln!(self.bytes, "M 100644 :{blob} {path}").expect("memory");
340 }
341 self.bytes.push(b'\n');
342 mark
343 }
344
345 fn run(self, repo: &Path) -> Result<()> {
346 git_input(repo, &["fast-import", "--quiet"], &self.bytes).map(|_| ())
347 }
348}
349
350fn invalid(reason: impl Into<String>) -> Error {
351 Error::Invalid {
352 reason: reason.into(),
353 }
354}
355
356fn io(error: std::io::Error) -> Error {
357 invalid(error.to_string())
358}
359
360fn git(repo: &Path, args: &[&str]) -> Result<String> {
361 git_input(repo, args, &[])
362}
363
364fn git_input(repo: &Path, args: &[&str], input: &[u8]) -> Result<String> {
365 let home = repo
366 .ancestors()
367 .find(|ancestor| ancestor.join(".gitconfig").is_file())
368 .unwrap_or(repo);
369 let mut child = Command::new("git")
370 .current_dir(repo)
371 .args(args)
372 .env_clear()
373 .env("PATH", std::env::var_os("PATH").unwrap_or_default())
374 .env("HOME", home)
375 .env("GIT_CONFIG_NOSYSTEM", "1")
376 .stdin(Stdio::piped())
377 .stdout(Stdio::piped())
378 .stderr(Stdio::piped())
379 .spawn()
380 .map_err(io)?;
381 child
382 .stdin
383 .take()
384 .expect("piped stdin")
385 .write_all(input)
386 .map_err(io)?;
387 let output = child.wait_with_output().map_err(io)?;
388 if !output.status.success() {
389 return Err(invalid(format!(
390 "workload git {} in {}: {}",
391 args.join(" "),
392 repo.display(),
393 String::from_utf8_lossy(&output.stderr)
394 )));
395 }
396 String::from_utf8(output.stdout).map_err(|error| invalid(error.to_string()))
397}