Skip to main content

command_stream/commands/
yes.rs

1//! Virtual `yes` command implementation
2
3use crate::commands::{CommandContext, StreamChunk};
4use crate::utils::{trace_lazy, CommandResult};
5use tokio::time::Duration;
6
7/// Execute the yes command
8///
9/// Outputs a string repeatedly until cancelled.
10pub async fn yes(ctx: CommandContext) -> CommandResult {
11    let output_str = if ctx.args.is_empty() {
12        "y".to_string()
13    } else {
14        ctx.args.join(" ")
15    };
16
17    let line = format!("{}\n", output_str);
18
19    trace_lazy("VirtualCommand", || {
20        format!("yes: starting with output '{}'", output_str)
21    });
22
23    // If we have a streaming output channel, use it
24    if let Some(ref tx) = ctx.output_tx {
25        loop {
26            if ctx.is_cancelled() {
27                trace_lazy("VirtualCommand", || "yes: cancelled".to_string());
28                return CommandResult::error_with_code("", 130);
29            }
30
31            if tx.send(StreamChunk::Stdout(line.clone())).await.is_err() {
32                // Channel closed
33                break;
34            }
35
36            // Small delay to prevent overwhelming
37            tokio::time::sleep(Duration::from_micros(100)).await;
38        }
39    } else {
40        // Without streaming, just output a few lines and return
41        // This is a safety measure to prevent infinite output
42        let mut output = String::new();
43        let max_iterations = 1000;
44
45        for _ in 0..max_iterations {
46            if ctx.is_cancelled() {
47                trace_lazy("VirtualCommand", || "yes: cancelled".to_string());
48                return CommandResult::error_with_code("", 130);
49            }
50            output.push_str(&line);
51        }
52
53        return CommandResult::success(output);
54    }
55
56    CommandResult::error_with_code("", 130)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use std::sync::atomic::{AtomicBool, Ordering};
63    use std::sync::Arc;
64
65    #[tokio::test]
66    async fn test_yes_with_cancellation() {
67        let cancelled = Arc::new(AtomicBool::new(false));
68        let cancelled_clone = cancelled.clone();
69
70        let mut ctx = CommandContext::new(vec![]);
71        ctx.is_cancelled = Some(Box::new(move || cancelled_clone.load(Ordering::SeqCst)));
72
73        // Cancel after a short delay
74        tokio::spawn(async move {
75            tokio::time::sleep(Duration::from_millis(10)).await;
76            cancelled.store(true, Ordering::SeqCst);
77        });
78
79        let result = yes(ctx).await;
80
81        // Should have produced some output before being cancelled
82        assert!(!result.stdout.is_empty() || result.code == 130);
83    }
84
85    #[tokio::test]
86    async fn test_yes_custom_string() {
87        let cancelled = Arc::new(AtomicBool::new(false));
88        let cancelled_clone = cancelled.clone();
89
90        let mut ctx = CommandContext::new(vec!["hello".to_string()]);
91        ctx.is_cancelled = Some(Box::new(move || cancelled_clone.load(Ordering::SeqCst)));
92
93        // Cancel immediately
94        cancelled.store(true, Ordering::SeqCst);
95
96        let result = yes(ctx).await;
97        assert_eq!(result.code, 130);
98    }
99}