aster_forge_test/
suite.rs1use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::sync::OnceLock;
6
7#[derive(Debug, Clone)]
14pub struct TestContainerSuite {
15 name: String,
16 state_dir: PathBuf,
17 instance: String,
18}
19
20impl TestContainerSuite {
21 pub fn new(name: &str) -> Self {
26 assert!(
27 !name.is_empty()
28 && name
29 .bytes()
30 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'),
31 "test container suite name must be non-empty ascii alphanumeric or '-': {name:?}"
32 );
33 let state_dir = std::env::temp_dir().join(format!("aster-testcontainers-{name}"));
34 std::fs::create_dir_all(&state_dir).unwrap_or_else(|error| {
35 panic!(
36 "failed to create test container state dir {}: {error}",
37 state_dir.display()
38 )
39 });
40 Self {
41 name: name.to_string(),
42 state_dir,
43 instance: instance_id().to_string(),
44 }
45 }
46
47 pub fn name(&self) -> &str {
49 &self.name
50 }
51
52 pub fn state_dir(&self) -> &Path {
54 &self.state_dir
55 }
56
57 pub fn container_name(&self, service: &str) -> String {
59 format!("aster-test-{}-{}-{service}", self.name, self.instance)
60 }
61
62 pub(crate) fn lock_path(&self, service: &str) -> PathBuf {
63 self.state_dir
64 .join(format!("{}-{}-{service}.lock", self.name, self.instance))
65 }
66
67 pub(crate) fn state_path(&self, service: &str) -> PathBuf {
68 self.state_dir
69 .join(format!("{}-{}-{service}.json", self.name, self.instance))
70 }
71}
72
73fn instance_id() -> &'static str {
74 static INSTANCE: OnceLock<String> = OnceLock::new();
75 INSTANCE.get_or_init(|| {
76 let mut hasher = std::collections::hash_map::DefaultHasher::new();
77 std::env::current_dir()
78 .unwrap_or_else(|_| PathBuf::from("."))
79 .hash(&mut hasher);
80 format!("{:016x}", hasher.finish())
81 })
82}
83
84#[cfg(test)]
85mod tests {
86 use super::TestContainerSuite;
87
88 #[test]
89 fn suite_rejects_invalid_names() {
90 for name in ["", "has space", "has/slash", "中文"] {
91 let result = std::panic::catch_unwind(|| TestContainerSuite::new(name));
92 assert!(result.is_err(), "suite name {name:?} should be rejected");
93 }
94 }
95
96 #[test]
97 fn suite_builds_scoped_paths_and_container_names() {
98 let suite = TestContainerSuite::new("forge-test");
99 assert_eq!(suite.name(), "forge-test");
100 assert!(
101 suite
102 .state_dir()
103 .ends_with("aster-testcontainers-forge-test")
104 );
105
106 let container = suite.container_name("redis");
107 assert!(container.starts_with("aster-test-forge-test-"));
108 assert!(container.ends_with("-redis"));
109 assert_ne!(suite.lock_path("redis"), suite.state_path("redis"));
110 }
111}