1use std::time::Duration;
4
5use tokio::io::{AsyncReadExt, AsyncWriteExt};
6
7use super::definition::{Dialog, DialogStep};
8use crate::Pattern;
9use crate::error::{ExpectError, Result};
10use crate::expect::PatternSet;
11use crate::session::Session;
12
13#[derive(Debug, Clone)]
15pub struct StepResult {
16 pub step_name: String,
18 pub success: bool,
20 pub output: String,
22 pub matched: Option<String>,
24 pub send: Option<String>,
26 pub error: Option<String>,
28 pub next_step: Option<String>,
30}
31
32#[derive(Debug, Clone)]
34pub struct DialogResult {
35 pub dialog_name: String,
37 pub success: bool,
39 pub steps: Vec<StepResult>,
41 pub output: String,
43 pub error: Option<String>,
45}
46
47impl DialogResult {
48 #[must_use]
50 pub fn all_success(&self) -> bool {
51 self.steps.iter().all(|s| s.success)
52 }
53
54 #[must_use]
56 pub fn last_step(&self) -> Option<&StepResult> {
57 self.steps.last()
58 }
59
60 #[must_use]
62 pub fn get_step(&self, name: &str) -> Option<&StepResult> {
63 self.steps.iter().find(|s| s.step_name == name)
64 }
65}
66
67#[derive(Debug)]
69pub struct DialogExecutor {
70 pub max_steps: usize,
72 pub default_timeout: Duration,
74}
75
76impl Default for DialogExecutor {
77 fn default() -> Self {
78 Self {
79 max_steps: 100,
80 default_timeout: Duration::from_secs(30),
81 }
82 }
83}
84
85impl DialogExecutor {
86 #[must_use]
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 #[must_use]
94 pub const fn max_steps(mut self, max: usize) -> Self {
95 self.max_steps = max;
96 self
97 }
98
99 #[must_use]
101 pub const fn default_timeout(mut self, timeout: Duration) -> Self {
102 self.default_timeout = timeout;
103 self
104 }
105
106 #[must_use]
115 pub fn execute_step_sync(
116 &self,
117 step: &DialogStep,
118 dialog: &Dialog,
119 _buffer: &str,
120 ) -> StepResult {
121 let substituted_send = step.send.as_ref().map(|s| dialog.substitute(s));
122
123 StepResult {
124 step_name: step.name.clone(),
125 success: true,
126 output: String::new(),
127 matched: step.expect.clone(),
128 send: substituted_send,
129 error: None,
130 next_step: step.next.clone().or_else(|| {
131 dialog
133 .steps
134 .iter()
135 .position(|s| s.name == step.name)
136 .and_then(|i| dialog.steps.get(i + 1))
137 .map(|s| s.name.clone())
138 }),
139 }
140 }
141
142 #[must_use]
144 pub fn step_pattern(&self, step: &DialogStep, dialog: &Dialog) -> Option<Pattern> {
145 step.expect
146 .as_ref()
147 .map(|e| Pattern::literal(dialog.substitute(e)))
148 }
149
150 pub async fn execute<T>(
182 &self,
183 session: &mut Session<T>,
184 dialog: &Dialog,
185 ) -> Result<DialogResult>
186 where
187 T: AsyncReadExt + AsyncWriteExt + Unpin + Send,
188 {
189 if dialog.is_empty() {
190 return Ok(DialogResult {
191 dialog_name: dialog.name.clone(),
192 success: true,
193 steps: Vec::new(),
194 output: String::new(),
195 error: None,
196 });
197 }
198
199 let mut step_results = Vec::new();
200 let mut total_output = String::new();
201 let mut step_count = 0;
202
203 let mut current_step_idx = if let Some(ref entry) = dialog.entry {
205 dialog
206 .steps
207 .iter()
208 .position(|s| &s.name == entry)
209 .unwrap_or(0)
210 } else {
211 0
212 };
213
214 loop {
215 step_count += 1;
217 if step_count > self.max_steps {
218 return Ok(DialogResult {
219 dialog_name: dialog.name.clone(),
220 success: false,
221 steps: step_results,
222 output: total_output,
223 error: Some(format!("Exceeded maximum steps ({})", self.max_steps)),
224 });
225 }
226
227 let Some(step) = dialog.steps.get(current_step_idx) else {
229 break; };
231
232 let step_result = self.execute_step(session, step, dialog).await?;
234 let success = step_result.success;
235 total_output.push_str(&step_result.output);
236
237 let next_step = step_result.next_step.clone();
239 step_results.push(step_result);
240
241 if !success {
242 return Ok(DialogResult {
243 dialog_name: dialog.name.clone(),
244 success: false,
245 steps: step_results,
246 output: total_output,
247 error: Some(format!("Step '{}' failed", step.name)),
248 });
249 }
250
251 if let Some(next_name) = next_step {
253 if let Some(idx) = dialog.steps.iter().position(|s| s.name == next_name) {
254 current_step_idx = idx;
255 } else {
256 break;
258 }
259 } else {
260 current_step_idx += 1;
262 if current_step_idx >= dialog.steps.len() {
263 break;
264 }
265 }
266 }
267
268 Ok(DialogResult {
269 dialog_name: dialog.name.clone(),
270 success: true,
271 steps: step_results,
272 output: total_output,
273 error: None,
274 })
275 }
276
277 pub async fn execute_step<T>(
283 &self,
284 session: &mut Session<T>,
285 step: &DialogStep,
286 dialog: &Dialog,
287 ) -> Result<StepResult>
288 where
289 T: AsyncReadExt + AsyncWriteExt + Unpin + Send,
290 {
291 let timeout = step.timeout.unwrap_or(self.default_timeout);
292 let mut output = String::new();
293 let mut matched_text = None;
294
295 if let Some(ref expect_pattern) = step.expect {
297 let pattern = Pattern::literal(dialog.substitute(expect_pattern));
298 let mut patterns = PatternSet::new();
299 patterns.add(pattern).add(Pattern::timeout(timeout));
300
301 match session.expect_any(&patterns).await {
302 Ok(m) => {
303 output.clone_from(&m.before);
304 matched_text = Some(m.matched);
305 }
306 Err(ExpectError::Timeout { buffer, .. }) => {
307 if step.continue_on_timeout {
308 output = buffer;
309 } else {
310 return Ok(StepResult {
311 step_name: step.name.clone(),
312 success: false,
313 output: buffer,
314 matched: None,
315 send: None,
316 error: Some(format!(
317 "Timeout waiting for pattern '{expect_pattern}' after {timeout:?}"
318 )),
319 next_step: None,
320 });
321 }
322 }
323 Err(e) => return Err(e),
324 }
325 }
326
327 let mut next_step = step.next.clone();
329 if let Some(ref matched) = matched_text {
330 for (branch_pattern, branch_target) in &step.branches {
331 if matched.contains(branch_pattern) {
332 next_step = Some(branch_target.clone());
333 break;
334 }
335 }
336 }
337
338 let substituted_send = if let Some(ref send_text) = step.send {
340 let substituted = dialog.substitute(send_text);
341 session.send_str(&substituted).await?;
342 Some(substituted)
343 } else if let Some(ctrl) = step.send_control {
344 session.send_control(ctrl).await?;
345 Some(format!("<{ctrl:?}>"))
346 } else {
347 None
348 };
349
350 if next_step.is_none() {
352 next_step = dialog
353 .steps
354 .iter()
355 .position(|s| s.name == step.name)
356 .and_then(|i| dialog.steps.get(i + 1))
357 .map(|s| s.name.clone());
358 }
359
360 Ok(StepResult {
361 step_name: step.name.clone(),
362 success: true,
363 output,
364 matched: matched_text,
365 send: substituted_send,
366 error: None,
367 next_step,
368 })
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn executor_default() {
378 let executor = DialogExecutor::new();
379 assert_eq!(executor.max_steps, 100);
380 }
381
382 #[test]
383 fn step_result_success() {
384 let result = StepResult {
385 step_name: "test".to_string(),
386 success: true,
387 output: "output".to_string(),
388 matched: Some("match".to_string()),
389 send: Some("hello".to_string()),
390 error: None,
391 next_step: None,
392 };
393 assert!(result.success);
394 assert_eq!(result.send, Some("hello".to_string()));
395 }
396
397 #[test]
398 fn step_result_with_substitution() {
399 use super::super::definition::{Dialog, DialogStep};
400
401 let dialog = Dialog::named("test_dialog")
402 .variable("username", "admin")
403 .step(
404 DialogStep::new("login")
405 .with_expect("Username:")
406 .with_send("${username}"),
407 );
408
409 let executor = DialogExecutor::new();
410 let step = &dialog.steps[0];
411 let result = executor.execute_step_sync(step, &dialog, "");
412
413 assert_eq!(result.step_name, "login");
414 assert_eq!(result.send, Some("admin".to_string()));
415 }
416}