rch_common/
mock_worker.rs1use crate::mock::{
8 MockConfig, MockRsyncConfig, clear_mock_overrides, set_mock_enabled_override,
9 set_mock_rsync_config_override, set_mock_ssh_config_override,
10};
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13static MOCK_WORKER_COUNTER: AtomicUsize = AtomicUsize::new(0);
14
15#[derive(Debug, Clone)]
16pub struct MockWorkerServer {
17 uri: String,
18 ssh_config: MockConfig,
19 rsync_config: MockRsyncConfig,
20 started: bool,
21}
22
23impl MockWorkerServer {
24 pub fn builder() -> MockWorkerServerBuilder {
25 MockWorkerServerBuilder::default()
26 }
27
28 pub fn uri(&self) -> &str {
30 &self.uri
31 }
32
33 pub fn start(&mut self) {
35 if self.started {
36 return;
37 }
38 set_mock_enabled_override(Some(true));
39 set_mock_ssh_config_override(Some(self.ssh_config.clone()));
40 set_mock_rsync_config_override(Some(self.rsync_config.clone()));
41 self.started = true;
42 }
43
44 pub fn stop(&mut self) {
46 if !self.started {
47 return;
48 }
49 clear_mock_overrides();
50 self.started = false;
51 }
52}
53
54impl Drop for MockWorkerServer {
55 fn drop(&mut self) {
56 self.stop();
57 }
58}
59
60#[derive(Debug, Clone)]
61pub struct MockWorkerServerBuilder {
62 uri: Option<String>,
63 ssh_config: MockConfig,
64 rsync_config: MockRsyncConfig,
65}
66
67impl Default for MockWorkerServerBuilder {
68 fn default() -> Self {
69 Self {
70 uri: None,
71 ssh_config: MockConfig::success(),
72 rsync_config: MockRsyncConfig::success(),
73 }
74 }
75}
76
77impl MockWorkerServerBuilder {
78 pub fn bind(mut self, uri: impl Into<String>) -> Self {
80 self.uri = Some(normalize_uri(uri.into()));
81 self
82 }
83
84 pub fn ssh_config(mut self, config: MockConfig) -> Self {
86 self.ssh_config = config;
87 self
88 }
89
90 pub fn rsync_config(mut self, config: MockRsyncConfig) -> Self {
92 self.rsync_config = config;
93 self
94 }
95
96 pub fn build(self) -> MockWorkerServer {
97 MockWorkerServer {
98 uri: self.uri.unwrap_or_else(default_uri),
99 ssh_config: self.ssh_config,
100 rsync_config: self.rsync_config,
101 started: false,
102 }
103 }
104}
105
106fn default_uri() -> String {
107 let id = MOCK_WORKER_COUNTER.fetch_add(1, Ordering::SeqCst) + 1;
108 format!("mock://worker-{}", id)
109}
110
111fn normalize_uri(uri: String) -> String {
112 if uri.starts_with("mock://") {
113 uri
114 } else {
115 format!("mock://{}", uri)
116 }
117}
118
119#[cfg(test)]
120#[allow(unsafe_code)]
121mod tests {
122 use super::*;
123 use crate::mock::{clear_mock_overrides, clear_thread_mock_override, is_mock_enabled};
124 use std::env;
125
126 fn clear_env() {
127 unsafe { env::remove_var("RCH_MOCK_SSH") };
129 clear_thread_mock_override();
130 }
131
132 #[test]
133 fn test_default_uri_prefix_and_uniqueness() {
134 let server_a = MockWorkerServer::builder().build();
135 let server_b = MockWorkerServer::builder().build();
136
137 assert!(server_a.uri().starts_with("mock://worker-"));
138 assert!(server_b.uri().starts_with("mock://worker-"));
139 assert_ne!(server_a.uri(), server_b.uri());
140 }
141
142 #[test]
143 fn test_bind_normalizes_uri() {
144 let server = MockWorkerServer::builder().bind("localhost:9900").build();
145 assert_eq!(server.uri(), "mock://localhost:9900");
146 }
147
148 #[test]
149 fn test_bind_preserves_mock_prefix() {
150 let server = MockWorkerServer::builder()
151 .bind("mock://example:1234")
152 .build();
153 assert_eq!(server.uri(), "mock://example:1234");
154 }
155
156 #[test]
157 fn test_start_stop_toggles_mock_enabled() {
158 clear_env();
159 clear_mock_overrides();
160
161 let mut server = MockWorkerServer::builder().bind("worker-a").build();
162 assert!(!is_mock_enabled());
163
164 server.start();
165 assert!(is_mock_enabled());
166
167 server.stop();
168 assert!(!is_mock_enabled());
169
170 clear_mock_overrides();
171 }
172
173 #[test]
178 fn test_mock_worker_server_debug() {
179 let server = MockWorkerServer::builder().bind("debug-worker").build();
180 let debug_str = format!("{:?}", server);
181 assert!(debug_str.contains("MockWorkerServer"));
182 assert!(debug_str.contains("uri"));
183 assert!(debug_str.contains("mock://debug-worker"));
184 }
185
186 #[test]
187 fn test_mock_worker_server_clone() {
188 let server = MockWorkerServer::builder().bind("clone-worker").build();
189 let cloned = server.clone();
190 assert_eq!(cloned.uri(), "mock://clone-worker");
191 }
192
193 #[test]
198 fn test_builder_debug() {
199 let builder = MockWorkerServer::builder().bind("builder-debug");
200 let debug_str = format!("{:?}", builder);
201 assert!(debug_str.contains("MockWorkerServerBuilder"));
202 }
203
204 #[test]
205 fn test_builder_clone() {
206 let builder = MockWorkerServer::builder().bind("builder-clone");
207 let cloned = builder.clone();
208 let server = cloned.build();
209 assert_eq!(server.uri(), "mock://builder-clone");
210 }
211
212 #[test]
213 fn test_builder_default() {
214 let builder = MockWorkerServerBuilder::default();
215 let server = builder.build();
216 assert!(server.uri().starts_with("mock://worker-"));
218 }
219
220 #[test]
225 fn test_builder_ssh_config() {
226 let custom_config = MockConfig::connection_failure();
227 let server = MockWorkerServer::builder()
228 .ssh_config(custom_config.clone())
229 .build();
230 let debug_str = format!("{:?}", server);
232 assert!(debug_str.contains("ssh_config"));
233 }
234
235 #[test]
236 fn test_builder_rsync_config() {
237 let custom_config = MockRsyncConfig::sync_failure();
238 let server = MockWorkerServer::builder()
239 .rsync_config(custom_config.clone())
240 .build();
241 let debug_str = format!("{:?}", server);
243 assert!(debug_str.contains("rsync_config"));
244 }
245
246 #[test]
247 fn test_builder_method_chaining() {
248 let server = MockWorkerServer::builder()
249 .bind("chained-worker")
250 .ssh_config(MockConfig::success())
251 .rsync_config(MockRsyncConfig::success())
252 .build();
253 assert_eq!(server.uri(), "mock://chained-worker");
254 }
255
256 #[test]
261 fn test_start_is_idempotent() {
262 clear_env();
263 clear_mock_overrides();
264
265 let mut server = MockWorkerServer::builder().bind("idempotent-start").build();
266
267 server.start();
269 assert!(is_mock_enabled());
270 server.start(); assert!(is_mock_enabled());
272 server.start(); assert!(is_mock_enabled());
274
275 server.stop();
276 clear_mock_overrides();
277 }
278
279 #[test]
280 fn test_stop_is_idempotent() {
281 clear_env();
282 clear_mock_overrides();
283
284 let mut server = MockWorkerServer::builder().bind("idempotent-stop").build();
285
286 server.stop();
288 assert!(!is_mock_enabled());
289 server.stop(); assert!(!is_mock_enabled());
291
292 clear_mock_overrides();
293 }
294
295 #[test]
296 fn test_stop_without_start_is_safe() {
297 clear_env();
298 clear_mock_overrides();
299
300 let mut server = MockWorkerServer::builder().bind("no-start-stop").build();
301 server.stop();
303 assert!(!is_mock_enabled());
304
305 clear_mock_overrides();
306 }
307
308 #[test]
313 fn test_drop_clears_mock_state() {
314 clear_env();
315 clear_mock_overrides();
316
317 {
318 let mut server = MockWorkerServer::builder().bind("drop-test").build();
319 server.start();
320 assert!(is_mock_enabled());
321 }
323
324 assert!(!is_mock_enabled());
326
327 clear_mock_overrides();
328 }
329
330 #[test]
335 fn test_normalize_uri_without_prefix() {
336 assert_eq!(
337 normalize_uri("localhost:9900".to_string()),
338 "mock://localhost:9900"
339 );
340 }
341
342 #[test]
343 fn test_normalize_uri_with_prefix() {
344 assert_eq!(
345 normalize_uri("mock://already-prefixed".to_string()),
346 "mock://already-prefixed"
347 );
348 }
349
350 #[test]
351 fn test_normalize_uri_empty() {
352 assert_eq!(normalize_uri("".to_string()), "mock://");
353 }
354}