cortexkit_test_support/
stable.rs1#[cfg(unix)]
2use std::path::PathBuf;
3
4#[cfg(unix)]
8pub fn stable_executable_dir(files: &[(&str, &[u8])]) -> PathBuf {
9 use sha2::{Digest, Sha256};
10 use std::os::unix::fs::PermissionsExt;
11 use std::sync::atomic::{AtomicU64, Ordering};
12 static NEXT: AtomicU64 = AtomicU64::new(0);
13 let mut hash = Sha256::new();
14 for (name, bytes) in files {
15 assert!(
16 !name.is_empty()
17 && !name.contains('/')
18 && !name.contains('\\')
19 && !name.starts_with('.'),
20 "stable executable name must be plain"
21 );
22 for piece in [name.as_bytes(), *bytes] {
23 hash.update((piece.len() as u64).to_le_bytes());
24 hash.update(piece);
25 }
26 }
27 let key = format!("{:x}", hash.finalize());
28 let dir = crate::shared_scratch_root()
29 .join(crate::STABLE_BIN_DIR)
30 .join(key);
31 std::fs::create_dir_all(&dir).unwrap();
32 for (name, bytes) in files {
33 let path = dir.join(name);
34 let current = std::fs::metadata(&path)
35 .ok()
36 .filter(|m| m.permissions().mode() & 0o777 == 0o755)
37 .and_then(|_| std::fs::read(&path).ok());
38 if current.as_deref() == Some(*bytes) {
39 continue;
40 }
41 let nonce = NEXT.fetch_add(1, Ordering::Relaxed);
42 let input = dir.join(format!(".input-{}-{nonce}", std::process::id()));
43 let staging = dir.join(format!(".stage-{}-{nonce}", std::process::id()));
44 std::fs::write(&input, bytes).unwrap();
45 crate::checked_output(std::process::Command::new("cp").arg(&input).arg(&staging)).unwrap();
46 std::fs::remove_file(input).unwrap();
47 std::fs::set_permissions(&staging, std::fs::Permissions::from_mode(0o755)).unwrap();
48 std::fs::rename(staging, &path).unwrap();
49 }
50 dir
51}
52#[cfg(unix)]
54pub fn stable_executable(name: &str, contents: impl AsRef<[u8]>) -> PathBuf {
55 stable_executable_dir(&[(name, contents.as_ref())]).join(name)
56}
57
58#[cfg(all(test, unix))]
59mod tests {
60 use super::*;
61 #[test]
62 fn stable_executable_from_eight_threads_leaves_one_file_with_the_right_bytes() {
63 use std::os::unix::fs::{MetadataExt, PermissionsExt};
64 let body = format!(
65 "#!/bin/sh\n# {}-{:?}\nexit 0\n",
66 std::process::id(),
67 std::time::SystemTime::now()
68 );
69 let barrier = std::sync::Arc::new(std::sync::Barrier::new(8));
70 let threads: Vec<_> = (0..8)
71 .map(|_| {
72 let barrier = barrier.clone();
73 let body = body.clone();
74 std::thread::spawn(move || {
75 barrier.wait();
76 stable_executable("probe", body)
77 })
78 })
79 .collect();
80 let paths: Vec<_> = threads.into_iter().map(|t| t.join().unwrap()).collect();
81 assert!(paths.iter().all(|p| p == &paths[0]));
82 let path = &paths[0];
83 assert!(path.starts_with(crate::shared_scratch_root().join(crate::STABLE_BIN_DIR)));
84 assert_eq!(
85 std::fs::read_dir(path.parent().unwrap()).unwrap().count(),
86 1
87 );
88 assert_eq!(std::fs::read(path).unwrap(), body.as_bytes());
89 let meta = std::fs::metadata(path).unwrap();
90 assert_eq!(meta.permissions().mode() & 0o777, 0o755);
91 assert_eq!(stable_executable("probe", body), *path);
92 assert_eq!(std::fs::metadata(path).unwrap().ino(), meta.ino());
93 }
94}