command-stream 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
package/rust/src/utils.rs
CHANGED
|
@@ -1,55 +1,25 @@
|
|
|
1
1
|
//! Utility functions and types for command-stream
|
|
2
2
|
//!
|
|
3
|
-
//! This module provides helper functions for
|
|
4
|
-
//! and
|
|
3
|
+
//! This module provides helper functions for command results, virtual command
|
|
4
|
+
//! utilities, and re-exports from specialized utility modules.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Module Organization
|
|
7
|
+
//!
|
|
8
|
+
//! The utilities are organized into focused modules following the same
|
|
9
|
+
//! modular pattern as the JavaScript implementation:
|
|
10
|
+
//!
|
|
11
|
+
//! - `trace` - Logging and tracing utilities
|
|
12
|
+
//! - `ansi` - ANSI escape code handling
|
|
13
|
+
//! - `quote` - Shell quoting utilities
|
|
14
|
+
//! - `utils` (this module) - Command results and virtual command helpers
|
|
5
15
|
|
|
6
16
|
use std::env;
|
|
7
17
|
use std::path::{Path, PathBuf};
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/// - COMMAND_STREAM_VERBOSE=true (enables tracing unless TRACE=false)
|
|
14
|
-
pub fn is_trace_enabled() -> bool {
|
|
15
|
-
let trace_env = env::var("COMMAND_STREAM_TRACE").ok();
|
|
16
|
-
let verbose_env = env::var("COMMAND_STREAM_VERBOSE")
|
|
17
|
-
.map(|v| v == "true")
|
|
18
|
-
.unwrap_or(false);
|
|
19
|
-
|
|
20
|
-
match trace_env.as_deref() {
|
|
21
|
-
Some("false") => false,
|
|
22
|
-
Some("true") => true,
|
|
23
|
-
_ => verbose_env,
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Trace function for verbose logging
|
|
28
|
-
///
|
|
29
|
-
/// Outputs trace messages to stderr when tracing is enabled.
|
|
30
|
-
/// Messages are prefixed with timestamp and category.
|
|
31
|
-
pub fn trace(category: &str, message: &str) {
|
|
32
|
-
if !is_trace_enabled() {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let timestamp = chrono::Utc::now().to_rfc3339();
|
|
37
|
-
eprintln!("[TRACE {}] [{}] {}", timestamp, category, message);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// Trace function with lazy message evaluation
|
|
41
|
-
///
|
|
42
|
-
/// Only evaluates the message function if tracing is enabled.
|
|
43
|
-
pub fn trace_lazy<F>(category: &str, message_fn: F)
|
|
44
|
-
where
|
|
45
|
-
F: FnOnce() -> String,
|
|
46
|
-
{
|
|
47
|
-
if !is_trace_enabled() {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
trace(category, &message_fn());
|
|
52
|
-
}
|
|
19
|
+
// Re-export from specialized modules for backwards compatibility
|
|
20
|
+
pub use crate::trace::{is_trace_enabled, trace, trace_lazy};
|
|
21
|
+
pub use crate::ansi::{AnsiConfig, AnsiUtils};
|
|
22
|
+
pub use crate::quote::quote;
|
|
53
23
|
|
|
54
24
|
/// Result type for virtual command operations
|
|
55
25
|
#[derive(Debug, Clone)]
|
|
@@ -167,101 +137,6 @@ impl VirtualUtils {
|
|
|
167
137
|
}
|
|
168
138
|
}
|
|
169
139
|
|
|
170
|
-
/// ANSI control character utilities
|
|
171
|
-
pub struct AnsiUtils;
|
|
172
|
-
|
|
173
|
-
impl AnsiUtils {
|
|
174
|
-
/// Strip ANSI escape sequences from text
|
|
175
|
-
pub fn strip_ansi(text: &str) -> String {
|
|
176
|
-
let re = regex::Regex::new(r"\x1b\[[0-9;]*[mGKHFJ]").unwrap();
|
|
177
|
-
re.replace_all(text, "").to_string()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/// Strip control characters from text, preserving newlines, carriage returns, and tabs
|
|
181
|
-
pub fn strip_control_chars(text: &str) -> String {
|
|
182
|
-
text.chars()
|
|
183
|
-
.filter(|c| {
|
|
184
|
-
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
|
|
185
|
-
!matches!(*c as u32,
|
|
186
|
-
0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F
|
|
187
|
-
)
|
|
188
|
-
})
|
|
189
|
-
.collect()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/// Strip both ANSI sequences and control characters
|
|
193
|
-
pub fn strip_all(text: &str) -> String {
|
|
194
|
-
Self::strip_control_chars(&Self::strip_ansi(text))
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/// Clean data for processing (strips ANSI and control chars)
|
|
198
|
-
pub fn clean_for_processing(data: &str) -> String {
|
|
199
|
-
Self::strip_all(data)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/// Configuration for ANSI handling
|
|
204
|
-
#[derive(Debug, Clone)]
|
|
205
|
-
pub struct AnsiConfig {
|
|
206
|
-
pub preserve_ansi: bool,
|
|
207
|
-
pub preserve_control_chars: bool,
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
impl Default for AnsiConfig {
|
|
211
|
-
fn default() -> Self {
|
|
212
|
-
AnsiConfig {
|
|
213
|
-
preserve_ansi: true,
|
|
214
|
-
preserve_control_chars: true,
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
impl AnsiConfig {
|
|
220
|
-
/// Process output according to config settings
|
|
221
|
-
pub fn process_output(&self, data: &str) -> String {
|
|
222
|
-
if !self.preserve_ansi && !self.preserve_control_chars {
|
|
223
|
-
AnsiUtils::clean_for_processing(data)
|
|
224
|
-
} else if !self.preserve_ansi {
|
|
225
|
-
AnsiUtils::strip_ansi(data)
|
|
226
|
-
} else if !self.preserve_control_chars {
|
|
227
|
-
AnsiUtils::strip_control_chars(data)
|
|
228
|
-
} else {
|
|
229
|
-
data.to_string()
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/// Quote a value for safe shell usage
|
|
235
|
-
pub fn quote(value: &str) -> String {
|
|
236
|
-
if value.is_empty() {
|
|
237
|
-
return "''".to_string();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// If already properly quoted, check if we can use as-is
|
|
241
|
-
if value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2 {
|
|
242
|
-
let inner = &value[1..value.len() - 1];
|
|
243
|
-
if !inner.contains('\'') {
|
|
244
|
-
return value.to_string();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if value.starts_with('"') && value.ends_with('"') && value.len() > 2 {
|
|
249
|
-
// If already double-quoted, wrap in single quotes
|
|
250
|
-
return format!("'{}'", value);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Check if the string needs quoting at all
|
|
254
|
-
// Safe characters: alphanumeric, dash, underscore, dot, slash, colon, equals, comma, plus
|
|
255
|
-
let safe_pattern = regex::Regex::new(r"^[a-zA-Z0-9_\-./=,+@:]+$").unwrap();
|
|
256
|
-
|
|
257
|
-
if safe_pattern.is_match(value) {
|
|
258
|
-
return value.to_string();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Default behavior: wrap in single quotes and escape any internal single quotes
|
|
262
|
-
format!("'{}'", value.replace('\'', "'\\''"))
|
|
263
|
-
}
|
|
264
|
-
|
|
265
140
|
#[cfg(test)]
|
|
266
141
|
mod tests {
|
|
267
142
|
use super::*;
|
|
@@ -284,6 +159,13 @@ mod tests {
|
|
|
284
159
|
assert_eq!(result.code, 1);
|
|
285
160
|
}
|
|
286
161
|
|
|
162
|
+
#[test]
|
|
163
|
+
fn test_command_result_error_with_code() {
|
|
164
|
+
let result = CommandResult::error_with_code("permission denied", 126);
|
|
165
|
+
assert!(!result.is_success());
|
|
166
|
+
assert_eq!(result.code, 126);
|
|
167
|
+
}
|
|
168
|
+
|
|
287
169
|
#[test]
|
|
288
170
|
fn test_resolve_path_absolute() {
|
|
289
171
|
let path = VirtualUtils::resolve_path("/absolute/path", None);
|
|
@@ -298,38 +180,51 @@ mod tests {
|
|
|
298
180
|
}
|
|
299
181
|
|
|
300
182
|
#[test]
|
|
301
|
-
fn
|
|
302
|
-
let
|
|
303
|
-
|
|
183
|
+
fn test_validate_args_success() {
|
|
184
|
+
let args = vec!["arg1".to_string()];
|
|
185
|
+
assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none());
|
|
304
186
|
}
|
|
305
187
|
|
|
306
188
|
#[test]
|
|
307
|
-
fn
|
|
308
|
-
let
|
|
309
|
-
|
|
189
|
+
fn test_validate_args_missing() {
|
|
190
|
+
let args = vec!["arg1".to_string()];
|
|
191
|
+
let result = VirtualUtils::validate_args(&args, 2, "cmd");
|
|
192
|
+
assert!(result.is_some());
|
|
310
193
|
}
|
|
311
194
|
|
|
312
195
|
#[test]
|
|
313
|
-
fn
|
|
314
|
-
|
|
196
|
+
fn test_missing_operand_error() {
|
|
197
|
+
let result = VirtualUtils::missing_operand_error("cat");
|
|
198
|
+
assert!(!result.is_success());
|
|
199
|
+
assert!(result.stderr.contains("missing operand"));
|
|
315
200
|
}
|
|
316
201
|
|
|
317
202
|
#[test]
|
|
318
|
-
fn
|
|
319
|
-
|
|
320
|
-
|
|
203
|
+
fn test_invalid_argument_error() {
|
|
204
|
+
let result = VirtualUtils::invalid_argument_error("ls", "invalid option");
|
|
205
|
+
assert!(!result.is_success());
|
|
206
|
+
assert!(result.stderr.contains("invalid option"));
|
|
321
207
|
}
|
|
322
208
|
|
|
209
|
+
// Re-exported module tests are in their respective modules
|
|
210
|
+
// These tests verify the re-exports work correctly
|
|
211
|
+
|
|
323
212
|
#[test]
|
|
324
|
-
fn
|
|
213
|
+
fn test_reexported_quote() {
|
|
214
|
+
assert_eq!(quote("hello"), "hello");
|
|
325
215
|
assert_eq!(quote("hello world"), "'hello world'");
|
|
326
|
-
assert_eq!(quote("it's"), "'it'\\''s'");
|
|
327
216
|
}
|
|
328
217
|
|
|
329
218
|
#[test]
|
|
330
|
-
fn
|
|
331
|
-
let
|
|
332
|
-
|
|
333
|
-
|
|
219
|
+
fn test_reexported_ansi_utils() {
|
|
220
|
+
let text = "\x1b[31mRed text\x1b[0m";
|
|
221
|
+
assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn test_reexported_ansi_config() {
|
|
226
|
+
let config = AnsiConfig::default();
|
|
227
|
+
assert!(config.preserve_ansi);
|
|
228
|
+
assert!(config.preserve_control_chars);
|
|
334
229
|
}
|
|
335
230
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
//! Integration tests for the events module
|
|
2
|
+
|
|
3
|
+
use command_stream::{EventData, EventType, StreamEmitter};
|
|
4
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
|
|
7
|
+
#[tokio::test]
|
|
8
|
+
async fn test_stream_emitter_creation() {
|
|
9
|
+
let emitter = StreamEmitter::new();
|
|
10
|
+
assert_eq!(emitter.listener_count(&EventType::Stdout).await, 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[tokio::test]
|
|
14
|
+
async fn test_stream_emitter_on_emit() {
|
|
15
|
+
let emitter = StreamEmitter::new();
|
|
16
|
+
let counter = Arc::new(AtomicUsize::new(0));
|
|
17
|
+
let counter_clone = counter.clone();
|
|
18
|
+
|
|
19
|
+
emitter
|
|
20
|
+
.on(EventType::Stdout, move |data| {
|
|
21
|
+
if let EventData::String(s) = data {
|
|
22
|
+
assert_eq!(s, "hello");
|
|
23
|
+
counter_clone.fetch_add(1, Ordering::SeqCst);
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.await;
|
|
27
|
+
|
|
28
|
+
emitter
|
|
29
|
+
.emit(EventType::Stdout, EventData::String("hello".to_string()))
|
|
30
|
+
.await;
|
|
31
|
+
|
|
32
|
+
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[tokio::test]
|
|
36
|
+
async fn test_stream_emitter_multiple_listeners() {
|
|
37
|
+
let emitter = StreamEmitter::new();
|
|
38
|
+
let counter1 = Arc::new(AtomicUsize::new(0));
|
|
39
|
+
let counter2 = Arc::new(AtomicUsize::new(0));
|
|
40
|
+
let c1 = counter1.clone();
|
|
41
|
+
let c2 = counter2.clone();
|
|
42
|
+
|
|
43
|
+
emitter
|
|
44
|
+
.on(EventType::Stdout, move |_| {
|
|
45
|
+
c1.fetch_add(1, Ordering::SeqCst);
|
|
46
|
+
})
|
|
47
|
+
.await;
|
|
48
|
+
|
|
49
|
+
emitter
|
|
50
|
+
.on(EventType::Stdout, move |_| {
|
|
51
|
+
c2.fetch_add(1, Ordering::SeqCst);
|
|
52
|
+
})
|
|
53
|
+
.await;
|
|
54
|
+
|
|
55
|
+
emitter
|
|
56
|
+
.emit(EventType::Stdout, EventData::String("test".to_string()))
|
|
57
|
+
.await;
|
|
58
|
+
|
|
59
|
+
assert_eq!(counter1.load(Ordering::SeqCst), 1);
|
|
60
|
+
assert_eq!(counter2.load(Ordering::SeqCst), 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[tokio::test]
|
|
64
|
+
async fn test_stream_emitter_once() {
|
|
65
|
+
let emitter = StreamEmitter::new();
|
|
66
|
+
let counter = Arc::new(AtomicUsize::new(0));
|
|
67
|
+
let counter_clone = counter.clone();
|
|
68
|
+
|
|
69
|
+
emitter
|
|
70
|
+
.once(EventType::Exit, move |_| {
|
|
71
|
+
counter_clone.fetch_add(1, Ordering::SeqCst);
|
|
72
|
+
})
|
|
73
|
+
.await;
|
|
74
|
+
|
|
75
|
+
// Emit multiple times
|
|
76
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(0)).await;
|
|
77
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(0)).await;
|
|
78
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(0)).await;
|
|
79
|
+
|
|
80
|
+
// Should only count once
|
|
81
|
+
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[tokio::test]
|
|
85
|
+
async fn test_stream_emitter_off() {
|
|
86
|
+
let emitter = StreamEmitter::new();
|
|
87
|
+
let counter = Arc::new(AtomicUsize::new(0));
|
|
88
|
+
let counter_clone = counter.clone();
|
|
89
|
+
|
|
90
|
+
emitter
|
|
91
|
+
.on(EventType::Stderr, move |_| {
|
|
92
|
+
counter_clone.fetch_add(1, Ordering::SeqCst);
|
|
93
|
+
})
|
|
94
|
+
.await;
|
|
95
|
+
|
|
96
|
+
assert_eq!(emitter.listener_count(&EventType::Stderr).await, 1);
|
|
97
|
+
|
|
98
|
+
emitter.off(EventType::Stderr).await;
|
|
99
|
+
|
|
100
|
+
assert_eq!(emitter.listener_count(&EventType::Stderr).await, 0);
|
|
101
|
+
|
|
102
|
+
// Emit after off - should not trigger listener
|
|
103
|
+
emitter
|
|
104
|
+
.emit(EventType::Stderr, EventData::String("error".to_string()))
|
|
105
|
+
.await;
|
|
106
|
+
|
|
107
|
+
assert_eq!(counter.load(Ordering::SeqCst), 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[tokio::test]
|
|
111
|
+
async fn test_stream_emitter_different_events() {
|
|
112
|
+
let emitter = StreamEmitter::new();
|
|
113
|
+
let stdout_counter = Arc::new(AtomicUsize::new(0));
|
|
114
|
+
let stderr_counter = Arc::new(AtomicUsize::new(0));
|
|
115
|
+
let exit_counter = Arc::new(AtomicUsize::new(0));
|
|
116
|
+
|
|
117
|
+
let out = stdout_counter.clone();
|
|
118
|
+
let err = stderr_counter.clone();
|
|
119
|
+
let exit = exit_counter.clone();
|
|
120
|
+
|
|
121
|
+
emitter
|
|
122
|
+
.on(EventType::Stdout, move |_| {
|
|
123
|
+
out.fetch_add(1, Ordering::SeqCst);
|
|
124
|
+
})
|
|
125
|
+
.await;
|
|
126
|
+
|
|
127
|
+
emitter
|
|
128
|
+
.on(EventType::Stderr, move |_| {
|
|
129
|
+
err.fetch_add(1, Ordering::SeqCst);
|
|
130
|
+
})
|
|
131
|
+
.await;
|
|
132
|
+
|
|
133
|
+
emitter
|
|
134
|
+
.on(EventType::Exit, move |_| {
|
|
135
|
+
exit.fetch_add(1, Ordering::SeqCst);
|
|
136
|
+
})
|
|
137
|
+
.await;
|
|
138
|
+
|
|
139
|
+
emitter
|
|
140
|
+
.emit(EventType::Stdout, EventData::String("out1".to_string()))
|
|
141
|
+
.await;
|
|
142
|
+
emitter
|
|
143
|
+
.emit(EventType::Stdout, EventData::String("out2".to_string()))
|
|
144
|
+
.await;
|
|
145
|
+
emitter
|
|
146
|
+
.emit(EventType::Stderr, EventData::String("err".to_string()))
|
|
147
|
+
.await;
|
|
148
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(0)).await;
|
|
149
|
+
|
|
150
|
+
assert_eq!(stdout_counter.load(Ordering::SeqCst), 2);
|
|
151
|
+
assert_eq!(stderr_counter.load(Ordering::SeqCst), 1);
|
|
152
|
+
assert_eq!(exit_counter.load(Ordering::SeqCst), 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[tokio::test]
|
|
156
|
+
async fn test_stream_emitter_remove_all_listeners() {
|
|
157
|
+
let emitter = StreamEmitter::new();
|
|
158
|
+
|
|
159
|
+
emitter.on(EventType::Stdout, |_| {}).await;
|
|
160
|
+
emitter.on(EventType::Stderr, |_| {}).await;
|
|
161
|
+
emitter.on(EventType::Exit, |_| {}).await;
|
|
162
|
+
|
|
163
|
+
assert!(emitter.listener_count(&EventType::Stdout).await > 0);
|
|
164
|
+
assert!(emitter.listener_count(&EventType::Stderr).await > 0);
|
|
165
|
+
assert!(emitter.listener_count(&EventType::Exit).await > 0);
|
|
166
|
+
|
|
167
|
+
emitter.remove_all_listeners().await;
|
|
168
|
+
|
|
169
|
+
assert_eq!(emitter.listener_count(&EventType::Stdout).await, 0);
|
|
170
|
+
assert_eq!(emitter.listener_count(&EventType::Stderr).await, 0);
|
|
171
|
+
assert_eq!(emitter.listener_count(&EventType::Exit).await, 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[tokio::test]
|
|
175
|
+
async fn test_event_data_variants() {
|
|
176
|
+
let emitter = StreamEmitter::new();
|
|
177
|
+
|
|
178
|
+
let string_received = Arc::new(std::sync::Mutex::new(None));
|
|
179
|
+
let code_received = Arc::new(std::sync::Mutex::new(None));
|
|
180
|
+
|
|
181
|
+
let str_clone = string_received.clone();
|
|
182
|
+
let code_clone = code_received.clone();
|
|
183
|
+
|
|
184
|
+
emitter
|
|
185
|
+
.on(EventType::Stdout, move |data| {
|
|
186
|
+
if let EventData::String(s) = data {
|
|
187
|
+
*str_clone.lock().unwrap() = Some(s);
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
.await;
|
|
191
|
+
|
|
192
|
+
emitter
|
|
193
|
+
.on(EventType::Exit, move |data| {
|
|
194
|
+
if let EventData::ExitCode(code) = data {
|
|
195
|
+
*code_clone.lock().unwrap() = Some(code);
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.await;
|
|
199
|
+
|
|
200
|
+
emitter
|
|
201
|
+
.emit(EventType::Stdout, EventData::String("test data".to_string()))
|
|
202
|
+
.await;
|
|
203
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(42)).await;
|
|
204
|
+
|
|
205
|
+
assert_eq!(*string_received.lock().unwrap(), Some("test data".to_string()));
|
|
206
|
+
assert_eq!(*code_received.lock().unwrap(), Some(42));
|
|
207
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//! Tests for the cmd! macro and its aliases (s!, sh!, cs!)
|
|
2
|
+
|
|
3
|
+
use command_stream::{cmd, sh, s, cs};
|
|
4
|
+
|
|
5
|
+
#[tokio::test]
|
|
6
|
+
async fn test_cmd_macro_simple() {
|
|
7
|
+
let result = cmd!("echo hello").await.unwrap();
|
|
8
|
+
assert!(result.is_success());
|
|
9
|
+
assert!(result.stdout.contains("hello"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[tokio::test]
|
|
13
|
+
async fn test_cmd_macro_with_interpolation() {
|
|
14
|
+
let name = "world";
|
|
15
|
+
let result = cmd!("echo hello {}", name).await.unwrap();
|
|
16
|
+
assert!(result.is_success());
|
|
17
|
+
assert!(result.stdout.contains("hello"));
|
|
18
|
+
assert!(result.stdout.contains("world"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[tokio::test]
|
|
22
|
+
async fn test_cmd_macro_with_multiple_interpolations() {
|
|
23
|
+
let greeting = "Hello";
|
|
24
|
+
let name = "World";
|
|
25
|
+
let result = cmd!("echo {} {}", greeting, name).await.unwrap();
|
|
26
|
+
assert!(result.is_success());
|
|
27
|
+
assert!(result.stdout.contains("Hello"));
|
|
28
|
+
assert!(result.stdout.contains("World"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[tokio::test]
|
|
32
|
+
async fn test_cmd_macro_with_special_chars() {
|
|
33
|
+
// Test that special characters are properly quoted
|
|
34
|
+
let filename = "test file with spaces.txt";
|
|
35
|
+
let result = cmd!("echo {}", filename).await.unwrap();
|
|
36
|
+
assert!(result.is_success());
|
|
37
|
+
// The output should contain the filename (quoted in the command)
|
|
38
|
+
assert!(result.stdout.contains("test file with spaces.txt"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[tokio::test]
|
|
42
|
+
async fn test_sh_macro_alias() {
|
|
43
|
+
let result = sh!("echo hello from sh").await.unwrap();
|
|
44
|
+
assert!(result.is_success());
|
|
45
|
+
assert!(result.stdout.contains("hello from sh"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[tokio::test]
|
|
49
|
+
async fn test_s_macro_alias() {
|
|
50
|
+
let result = s!("echo hello from s").await.unwrap();
|
|
51
|
+
assert!(result.is_success());
|
|
52
|
+
assert!(result.stdout.contains("hello from s"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[tokio::test]
|
|
56
|
+
async fn test_cs_macro_alias() {
|
|
57
|
+
let result = cs!("echo hello from cs").await.unwrap();
|
|
58
|
+
assert!(result.is_success());
|
|
59
|
+
assert!(result.stdout.contains("hello from cs"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[tokio::test]
|
|
63
|
+
async fn test_s_macro_with_interpolation() {
|
|
64
|
+
let name = "world";
|
|
65
|
+
let result = s!("echo hello {}", name).await.unwrap();
|
|
66
|
+
assert!(result.is_success());
|
|
67
|
+
assert!(result.stdout.contains("hello"));
|
|
68
|
+
assert!(result.stdout.contains("world"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[tokio::test]
|
|
72
|
+
async fn test_cmd_macro_with_numbers() {
|
|
73
|
+
let count = 42;
|
|
74
|
+
let result = cmd!("echo The answer is {}", count).await.unwrap();
|
|
75
|
+
assert!(result.is_success());
|
|
76
|
+
assert!(result.stdout.contains("42"));
|
|
77
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//! Tests for the Pipeline module
|
|
2
|
+
|
|
3
|
+
use command_stream::{Pipeline, PipelineExt, ProcessRunner, RunOptions};
|
|
4
|
+
|
|
5
|
+
#[tokio::test]
|
|
6
|
+
async fn test_pipeline_simple() {
|
|
7
|
+
let result = Pipeline::new()
|
|
8
|
+
.add("echo hello world")
|
|
9
|
+
.run()
|
|
10
|
+
.await
|
|
11
|
+
.unwrap();
|
|
12
|
+
|
|
13
|
+
assert!(result.is_success());
|
|
14
|
+
assert!(result.stdout.contains("hello world"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[tokio::test]
|
|
18
|
+
async fn test_pipeline_two_commands() {
|
|
19
|
+
let result = Pipeline::new()
|
|
20
|
+
.add("echo 'hello\nworld\nhello again'")
|
|
21
|
+
.add("grep hello")
|
|
22
|
+
.run()
|
|
23
|
+
.await
|
|
24
|
+
.unwrap();
|
|
25
|
+
|
|
26
|
+
assert!(result.is_success());
|
|
27
|
+
// The grep should filter to only lines containing "hello"
|
|
28
|
+
assert!(result.stdout.contains("hello"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[tokio::test]
|
|
32
|
+
async fn test_pipeline_with_stdin() {
|
|
33
|
+
let result = Pipeline::new()
|
|
34
|
+
.stdin("line1\nline2\nline3")
|
|
35
|
+
.add("cat")
|
|
36
|
+
.run()
|
|
37
|
+
.await
|
|
38
|
+
.unwrap();
|
|
39
|
+
|
|
40
|
+
assert!(result.is_success());
|
|
41
|
+
assert!(result.stdout.contains("line1"));
|
|
42
|
+
assert!(result.stdout.contains("line2"));
|
|
43
|
+
assert!(result.stdout.contains("line3"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[tokio::test]
|
|
47
|
+
async fn test_pipeline_three_commands() {
|
|
48
|
+
let result = Pipeline::new()
|
|
49
|
+
.add("echo 'apple\nbanana\napricot\nblueberry'")
|
|
50
|
+
.add("grep a")
|
|
51
|
+
.add("wc -l")
|
|
52
|
+
.run()
|
|
53
|
+
.await
|
|
54
|
+
.unwrap();
|
|
55
|
+
|
|
56
|
+
assert!(result.is_success());
|
|
57
|
+
// Should count lines containing 'a': apple, banana, apricot = 3 lines
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[tokio::test]
|
|
61
|
+
async fn test_pipeline_empty() {
|
|
62
|
+
let result = Pipeline::new().run().await.unwrap();
|
|
63
|
+
|
|
64
|
+
// Empty pipeline should return error
|
|
65
|
+
assert!(!result.is_success());
|
|
66
|
+
assert!(result.stderr.contains("No commands"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[tokio::test]
|
|
70
|
+
async fn test_pipeline_failure_propagation() {
|
|
71
|
+
let result = Pipeline::new()
|
|
72
|
+
.add("echo hello")
|
|
73
|
+
.add("false") // This command always fails
|
|
74
|
+
.add("echo should not reach here")
|
|
75
|
+
.run()
|
|
76
|
+
.await
|
|
77
|
+
.unwrap();
|
|
78
|
+
|
|
79
|
+
// Pipeline should fail because 'false' returns non-zero
|
|
80
|
+
assert!(!result.is_success());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[tokio::test]
|
|
84
|
+
async fn test_pipeline_builder_pattern() {
|
|
85
|
+
// Test the fluent API
|
|
86
|
+
let pipeline = Pipeline::new()
|
|
87
|
+
.add("echo test")
|
|
88
|
+
.mirror_output(false)
|
|
89
|
+
.capture_output(true);
|
|
90
|
+
|
|
91
|
+
let result = pipeline.run().await.unwrap();
|
|
92
|
+
assert!(result.is_success());
|
|
93
|
+
}
|