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