forge_core/testing/context/
daemon.rs1#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use sqlx::PgPool;
9use tokio::sync::{Mutex, watch};
10use uuid::Uuid;
11
12use super::super::mock_http::{MockHttp, MockRequest, MockResponse};
13use crate::Result;
14use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
15
16pub struct TestDaemonContext {
34 pub daemon_name: String,
36 pub instance_id: Uuid,
38 pool: Option<PgPool>,
40 http: Arc<MockHttp>,
42 pub shutdown_tx: watch::Sender<bool>,
44 shutdown_rx: Mutex<watch::Receiver<bool>>,
46 env_provider: Arc<MockEnvProvider>,
48}
49
50impl TestDaemonContext {
51 pub fn builder(daemon_name: impl Into<String>) -> TestDaemonContextBuilder {
53 TestDaemonContextBuilder::new(daemon_name)
54 }
55
56 pub fn db(&self) -> Option<&PgPool> {
58 self.pool.as_ref()
59 }
60
61 pub fn http(&self) -> &MockHttp {
63 &self.http
64 }
65
66 pub fn is_shutdown_requested(&self) -> bool {
68 self.shutdown_rx
69 .try_lock()
70 .map(|rx| *rx.borrow())
71 .unwrap_or(false)
72 }
73
74 pub fn request_shutdown(&self) {
76 let _ = self.shutdown_tx.send(true);
77 }
78
79 pub async fn shutdown_signal(&self) {
90 let mut rx = self.shutdown_rx.lock().await;
91 while !*rx.borrow_and_update() {
92 if rx.changed().await.is_err() {
93 break;
94 }
95 }
96 }
97
98 pub async fn heartbeat(&self) -> Result<()> {
100 Ok(())
101 }
102
103 pub fn env_mock(&self) -> &MockEnvProvider {
105 &self.env_provider
106 }
107}
108
109impl EnvAccess for TestDaemonContext {
110 fn env_provider(&self) -> &dyn EnvProvider {
111 self.env_provider.as_ref()
112 }
113}
114
115pub struct TestDaemonContextBuilder {
117 daemon_name: String,
118 instance_id: Option<Uuid>,
119 pool: Option<PgPool>,
120 http: MockHttp,
121 env_vars: HashMap<String, String>,
122}
123
124impl TestDaemonContextBuilder {
125 pub fn new(daemon_name: impl Into<String>) -> Self {
127 Self {
128 daemon_name: daemon_name.into(),
129 instance_id: None,
130 pool: None,
131 http: MockHttp::new(),
132 env_vars: HashMap::new(),
133 }
134 }
135
136 pub fn with_instance_id(mut self, id: Uuid) -> Self {
138 self.instance_id = Some(id);
139 self
140 }
141
142 pub fn with_pool(mut self, pool: PgPool) -> Self {
144 self.pool = Some(pool);
145 self
146 }
147
148 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
150 where
151 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
152 {
153 self.http.add_mock_sync(pattern, handler);
154 self
155 }
156
157 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
159 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
160 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
161 }
162
163 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
165 self.env_vars.insert(key.into(), value.into());
166 self
167 }
168
169 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
171 self.env_vars.extend(vars);
172 self
173 }
174
175 pub fn build(self) -> TestDaemonContext {
177 let (shutdown_tx, shutdown_rx) = watch::channel(false);
178
179 TestDaemonContext {
180 daemon_name: self.daemon_name,
181 instance_id: self.instance_id.unwrap_or_else(Uuid::new_v4),
182 pool: self.pool,
183 http: Arc::new(self.http),
184 shutdown_tx,
185 shutdown_rx: Mutex::new(shutdown_rx),
186 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_daemon_context_creation() {
197 let ctx = TestDaemonContext::builder("heartbeat_daemon").build();
198
199 assert_eq!(ctx.daemon_name, "heartbeat_daemon");
200 assert!(!ctx.is_shutdown_requested());
201 }
202
203 #[test]
204 fn test_shutdown_request() {
205 let ctx = TestDaemonContext::builder("test").build();
206
207 assert!(!ctx.is_shutdown_requested());
208 ctx.request_shutdown();
209 assert!(ctx.is_shutdown_requested());
210 }
211
212 #[tokio::test]
213 async fn test_shutdown_signal() {
214 let ctx = TestDaemonContext::builder("test").build();
215
216 let shutdown_tx = ctx.shutdown_tx.clone();
218 tokio::spawn(async move {
219 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
220 let _ = shutdown_tx.send(true);
221 });
222
223 tokio::time::timeout(std::time::Duration::from_millis(200), ctx.shutdown_signal())
225 .await
226 .expect("Shutdown signal should complete");
227
228 assert!(ctx.is_shutdown_requested());
229 }
230
231 #[test]
232 fn test_with_instance_id() {
233 let id = Uuid::new_v4();
234 let ctx = TestDaemonContext::builder("test")
235 .with_instance_id(id)
236 .build();
237
238 assert_eq!(ctx.instance_id, id);
239 }
240}