wabot_testing/
async_jobs.rs1use std::sync::Arc;
18
19use parking_lot::Mutex;
20use wabot_addon_async_in_memory::InMemoryJobRepository;
21use wabot_core::injection::Container;
22use wabot_feature_async::{
23 job, register_async_runtime, register_job_repository, AsyncError, CommandData,
24 CommandHandlerEntry, CommandRegistry, CronHandlerEntry, Job, JobRepository, JobRunner,
25};
26
27pub struct AsyncHarness {
40 container: Container,
41 repository: Arc<InMemoryJobRepository>,
42 registry: Arc<CommandRegistry>,
43 runner: Arc<JobRunner>,
44 ran: Mutex<Vec<String>>,
48}
49
50impl AsyncHarness {
51 pub fn builder() -> AsyncHarnessBuilder {
52 AsyncHarnessBuilder {
53 container: None,
54 commands: Vec::new(),
55 crons: Vec::new(),
56 }
57 }
58
59 pub fn container(&self) -> &Container {
62 &self.container
63 }
64
65 pub async fn execute<C: CommandData + serde::Serialize>(&self, command: &C) -> FinishedJob {
75 let payload = serde_json::to_value(command).expect("a serializable command");
76 self.execute_named(C::COMMAND_NAME, payload).await
77 }
78
79 pub async fn execute_named(
82 &self,
83 command_name: &str,
84 payload: serde_json::Value,
85 ) -> FinishedJob {
86 assert!(
87 self.registry.get(command_name).is_some(),
88 "AsyncHarness: no handler registered for command '{command_name}'. \
89 Registered: {:?}",
90 self.registry.command_names()
91 );
92
93 let job = job::new_job(job::JobData {
94 base: Default::default(),
95 command_name: command_name.to_string(),
96 command_data: payload,
97 scheduled_at: Some(chrono::Utc::now().timestamp_millis()),
98 started_at: None,
99 success_at: None,
100 failed_at: None,
101 retry_delays_seconds: self
102 .registry
103 .options_for(command_name)
104 .and_then(|o| o.retry_delays_seconds),
105 intent_number: None,
106 error: None,
107 acceptable_running_time_seconds: None,
108 stuck_retry_attempts: None,
109 dedup_key: None,
110 actor: wabot_core::audit::audit_actor(),
113 request_id: wabot_core::log_context::request_id(),
114 });
115
116 self.repository.create(&job).await.expect("stored");
117 self.ran.lock().push(job.id().to_string());
118 let result = self.runner.run(self.container.clone(), job.clone()).await;
119 let stored = self
120 .repository
121 .find(job.id())
122 .await
123 .expect("a readable repository")
124 .expect("the job it just ran");
125
126 FinishedJob {
127 job: stored,
128 run_error: result.err(),
129 }
130 }
131
132 pub async fn run_cron(&self, command_name: &str) -> FinishedJob {
135 self.execute_named(command_name, serde_json::Value::Null)
136 .await
137 }
138
139 pub async fn jobs(&self) -> Vec<Job> {
141 let ids = self.ran.lock().clone();
142 let mut jobs = Vec::with_capacity(ids.len());
143 for id in ids {
144 if let Ok(Some(job)) = self.repository.find(&id).await {
145 jobs.push(job);
146 }
147 }
148 jobs
149 }
150}
151
152pub struct FinishedJob {
154 pub job: Job,
155 pub run_error: Option<AsyncError>,
159}
160
161impl FinishedJob {
162 pub fn succeeded(&self) -> bool {
163 job::was_success(&self.job)
164 }
165
166 pub fn error(&self) -> Option<String> {
168 self.job.data().error.as_ref().map(|e| e.message.clone())
169 }
170
171 pub fn attempts(&self) -> u32 {
174 self.job.data().intent_number.unwrap_or(0)
175 }
176
177 pub fn retry_at_ms(&self) -> Option<i64> {
180 self.job
181 .data()
182 .failed_at
183 .is_none()
184 .then(|| self.job.data().scheduled_at)
185 .flatten()
186 }
187
188 pub fn assert_succeeded(&self) -> &Self {
190 assert!(
191 self.succeeded(),
192 "expected the job to succeed, but it {}",
193 match self.error() {
194 Some(message) => format!("failed: {message}"),
195 None => "did not finish".to_string(),
196 }
197 );
198 self
199 }
200}
201
202pub struct AsyncHarnessBuilder {
203 container: Option<Container>,
204 commands: Vec<CommandHandlerEntry>,
205 crons: Vec<CronHandlerEntry>,
206}
207
208impl AsyncHarnessBuilder {
209 pub fn container(mut self, container: Container) -> Self {
212 self.container = Some(container);
213 self
214 }
215
216 pub fn command(mut self, entry: CommandHandlerEntry) -> Self {
219 self.commands.push(entry);
220 self
221 }
222
223 pub fn cron(mut self, entry: CronHandlerEntry) -> Self {
225 self.crons.push(entry);
226 self
227 }
228
229 pub fn build(self) -> AsyncHarness {
230 let container = self.container.unwrap_or_default();
231 let repository = Arc::new(InMemoryJobRepository::new());
232 register_job_repository(&container, repository.clone());
233 register_async_runtime(&container);
234
235 let registry: Arc<CommandRegistry> = container.resolve();
236 for entry in self.commands {
237 registry.register(entry);
238 }
239 for entry in self.crons {
240 registry.register(wabot_feature_async::cron_command_entry(&entry));
243 }
244
245 let runner = Arc::new(JobRunner::new(repository.clone(), registry.clone()));
246
247 AsyncHarness {
248 container,
249 repository,
250 registry,
251 runner,
252 ran: Mutex::new(Vec::new()),
253 }
254 }
255}