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