Skip to main content

command_stream/commands/
test.rs

1//! Virtual `test` command implementation
2
3use crate::commands::CommandContext;
4use crate::utils::CommandResult;
5use std::fs;
6use std::path::Path;
7
8/// Execute the test command
9///
10/// Evaluates conditional expressions.
11pub async fn test(ctx: CommandContext) -> CommandResult {
12    if ctx.args.is_empty() {
13        return CommandResult::error_with_code("", 1);
14    }
15
16    let result = evaluate_expression(&ctx.args);
17
18    if result {
19        CommandResult::success_empty()
20    } else {
21        CommandResult::error_with_code("", 1)
22    }
23}
24
25fn evaluate_expression(args: &[String]) -> bool {
26    if args.is_empty() {
27        return false;
28    }
29
30    // Handle unary operators
31    if args.len() == 2 {
32        let op = &args[0];
33        let arg = &args[1];
34
35        return match op.as_str() {
36            "-e" => Path::new(arg).exists(),
37            "-f" => Path::new(arg).is_file(),
38            "-d" => Path::new(arg).is_dir(),
39            "-r" => {
40                // Check if readable (simplified)
41                fs::metadata(arg).is_ok()
42            }
43            "-w" => {
44                // Check if writable (simplified)
45                fs::metadata(arg)
46                    .map(|m| !m.permissions().readonly())
47                    .unwrap_or(false)
48            }
49            "-x" => {
50                // Check if executable (simplified - Unix only)
51                #[cfg(unix)]
52                {
53                    use std::os::unix::fs::PermissionsExt;
54                    fs::metadata(arg)
55                        .map(|m| m.permissions().mode() & 0o111 != 0)
56                        .unwrap_or(false)
57                }
58                #[cfg(not(unix))]
59                {
60                    Path::new(arg).exists()
61                }
62            }
63            "-s" => {
64                // Check if file has size > 0
65                fs::metadata(arg).map(|m| m.len() > 0).unwrap_or(false)
66            }
67            "-z" => arg.is_empty(),
68            "-n" => !arg.is_empty(),
69            "!" => !evaluate_expression(&args[1..]),
70            _ => false,
71        };
72    }
73
74    // Handle binary operators
75    if args.len() == 3 {
76        let left = &args[0];
77        let op = &args[1];
78        let right = &args[2];
79
80        return match op.as_str() {
81            "=" | "==" => left == right,
82            "!=" => left != right,
83            "-eq" => {
84                let l: i64 = left.parse().unwrap_or(0);
85                let r: i64 = right.parse().unwrap_or(0);
86                l == r
87            }
88            "-ne" => {
89                let l: i64 = left.parse().unwrap_or(0);
90                let r: i64 = right.parse().unwrap_or(0);
91                l != r
92            }
93            "-lt" => {
94                let l: i64 = left.parse().unwrap_or(0);
95                let r: i64 = right.parse().unwrap_or(0);
96                l < r
97            }
98            "-le" => {
99                let l: i64 = left.parse().unwrap_or(0);
100                let r: i64 = right.parse().unwrap_or(0);
101                l <= r
102            }
103            "-gt" => {
104                let l: i64 = left.parse().unwrap_or(0);
105                let r: i64 = right.parse().unwrap_or(0);
106                l > r
107            }
108            "-ge" => {
109                let l: i64 = left.parse().unwrap_or(0);
110                let r: i64 = right.parse().unwrap_or(0);
111                l >= r
112            }
113            _ => false,
114        };
115    }
116
117    // Single argument: true if non-empty
118    if args.len() == 1 {
119        return !args[0].is_empty();
120    }
121
122    false
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use tempfile::tempdir;
129
130    #[tokio::test]
131    async fn test_file_exists() {
132        let temp = tempdir().unwrap();
133        let file = temp.path().join("test.txt");
134        fs::write(&file, "test").unwrap();
135
136        let ctx = CommandContext::new(vec!["-e".to_string(), file.to_string_lossy().to_string()]);
137        let result = test(ctx).await;
138        assert!(result.is_success());
139    }
140
141    #[tokio::test]
142    async fn test_file_not_exists() {
143        let ctx = CommandContext::new(vec![
144            "-e".to_string(),
145            "/nonexistent/file/12345".to_string(),
146        ]);
147        let result = test(ctx).await;
148        assert!(!result.is_success());
149    }
150
151    #[tokio::test]
152    async fn test_string_equality() {
153        let ctx = CommandContext::new(vec![
154            "hello".to_string(),
155            "=".to_string(),
156            "hello".to_string(),
157        ]);
158        let result = test(ctx).await;
159        assert!(result.is_success());
160    }
161
162    #[tokio::test]
163    async fn test_string_inequality() {
164        let ctx = CommandContext::new(vec![
165            "hello".to_string(),
166            "!=".to_string(),
167            "world".to_string(),
168        ]);
169        let result = test(ctx).await;
170        assert!(result.is_success());
171    }
172
173    #[tokio::test]
174    async fn test_numeric_comparison() {
175        let ctx = CommandContext::new(vec!["5".to_string(), "-gt".to_string(), "3".to_string()]);
176        let result = test(ctx).await;
177        assert!(result.is_success());
178    }
179
180    #[tokio::test]
181    async fn test_empty_string() {
182        let ctx = CommandContext::new(vec!["-z".to_string(), "".to_string()]);
183        let result = test(ctx).await;
184        assert!(result.is_success());
185    }
186
187    #[tokio::test]
188    async fn test_non_empty_string() {
189        let ctx = CommandContext::new(vec!["-n".to_string(), "hello".to_string()]);
190        let result = test(ctx).await;
191        assert!(result.is_success());
192    }
193}