command-stream 0.9.0 → 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/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
//! Integration tests for the state module
|
|
2
|
+
|
|
3
|
+
use command_stream::state::{GlobalState, ShellSettings};
|
|
4
|
+
|
|
5
|
+
#[test]
|
|
6
|
+
fn test_shell_settings_default() {
|
|
7
|
+
let settings = ShellSettings::new();
|
|
8
|
+
assert!(!settings.errexit);
|
|
9
|
+
assert!(!settings.verbose);
|
|
10
|
+
assert!(!settings.xtrace);
|
|
11
|
+
assert!(!settings.pipefail);
|
|
12
|
+
assert!(!settings.nounset);
|
|
13
|
+
assert!(!settings.noglob);
|
|
14
|
+
assert!(!settings.allexport);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn test_shell_settings_set_short_flags() {
|
|
19
|
+
let mut settings = ShellSettings::new();
|
|
20
|
+
|
|
21
|
+
settings.set("e", true);
|
|
22
|
+
assert!(settings.errexit);
|
|
23
|
+
|
|
24
|
+
settings.set("v", true);
|
|
25
|
+
assert!(settings.verbose);
|
|
26
|
+
|
|
27
|
+
settings.set("x", true);
|
|
28
|
+
assert!(settings.xtrace);
|
|
29
|
+
|
|
30
|
+
settings.set("u", true);
|
|
31
|
+
assert!(settings.nounset);
|
|
32
|
+
|
|
33
|
+
settings.set("f", true);
|
|
34
|
+
assert!(settings.noglob);
|
|
35
|
+
|
|
36
|
+
settings.set("a", true);
|
|
37
|
+
assert!(settings.allexport);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn test_shell_settings_set_long_names() {
|
|
42
|
+
let mut settings = ShellSettings::new();
|
|
43
|
+
|
|
44
|
+
settings.set("errexit", true);
|
|
45
|
+
assert!(settings.errexit);
|
|
46
|
+
|
|
47
|
+
settings.set("verbose", true);
|
|
48
|
+
assert!(settings.verbose);
|
|
49
|
+
|
|
50
|
+
settings.set("xtrace", true);
|
|
51
|
+
assert!(settings.xtrace);
|
|
52
|
+
|
|
53
|
+
settings.set("nounset", true);
|
|
54
|
+
assert!(settings.nounset);
|
|
55
|
+
|
|
56
|
+
settings.set("pipefail", true);
|
|
57
|
+
assert!(settings.pipefail);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn test_shell_settings_enable_disable() {
|
|
62
|
+
let mut settings = ShellSettings::new();
|
|
63
|
+
|
|
64
|
+
settings.enable("errexit");
|
|
65
|
+
assert!(settings.errexit);
|
|
66
|
+
|
|
67
|
+
settings.disable("errexit");
|
|
68
|
+
assert!(!settings.errexit);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn test_shell_settings_reset() {
|
|
73
|
+
let mut settings = ShellSettings::new();
|
|
74
|
+
|
|
75
|
+
settings.enable("errexit");
|
|
76
|
+
settings.enable("verbose");
|
|
77
|
+
settings.enable("pipefail");
|
|
78
|
+
|
|
79
|
+
settings.reset();
|
|
80
|
+
|
|
81
|
+
assert!(!settings.errexit);
|
|
82
|
+
assert!(!settings.verbose);
|
|
83
|
+
assert!(!settings.pipefail);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[tokio::test]
|
|
87
|
+
async fn test_global_state_shell_settings() {
|
|
88
|
+
let state = GlobalState::new();
|
|
89
|
+
|
|
90
|
+
// Default settings
|
|
91
|
+
let settings = state.get_shell_settings().await;
|
|
92
|
+
assert!(!settings.errexit);
|
|
93
|
+
|
|
94
|
+
// Enable option
|
|
95
|
+
state.enable_shell_option("errexit").await;
|
|
96
|
+
let settings = state.get_shell_settings().await;
|
|
97
|
+
assert!(settings.errexit);
|
|
98
|
+
|
|
99
|
+
// Disable option
|
|
100
|
+
state.disable_shell_option("errexit").await;
|
|
101
|
+
let settings = state.get_shell_settings().await;
|
|
102
|
+
assert!(!settings.errexit);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[tokio::test]
|
|
106
|
+
async fn test_global_state_runner_registration() {
|
|
107
|
+
let state = GlobalState::new();
|
|
108
|
+
|
|
109
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
110
|
+
|
|
111
|
+
let id1 = state.register_runner().await;
|
|
112
|
+
assert_eq!(state.active_runner_count().await, 1);
|
|
113
|
+
|
|
114
|
+
let id2 = state.register_runner().await;
|
|
115
|
+
assert_eq!(state.active_runner_count().await, 2);
|
|
116
|
+
|
|
117
|
+
// IDs should be unique
|
|
118
|
+
assert!(id1 != id2);
|
|
119
|
+
|
|
120
|
+
state.unregister_runner(id1).await;
|
|
121
|
+
assert_eq!(state.active_runner_count().await, 1);
|
|
122
|
+
|
|
123
|
+
state.unregister_runner(id2).await;
|
|
124
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[tokio::test]
|
|
128
|
+
async fn test_global_state_virtual_commands() {
|
|
129
|
+
let state = GlobalState::new();
|
|
130
|
+
|
|
131
|
+
// Enabled by default
|
|
132
|
+
assert!(state.are_virtual_commands_enabled());
|
|
133
|
+
|
|
134
|
+
state.disable_virtual_commands();
|
|
135
|
+
assert!(!state.are_virtual_commands_enabled());
|
|
136
|
+
|
|
137
|
+
state.enable_virtual_commands();
|
|
138
|
+
assert!(state.are_virtual_commands_enabled());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[tokio::test]
|
|
142
|
+
async fn test_global_state_signal_handlers() {
|
|
143
|
+
let state = GlobalState::new();
|
|
144
|
+
|
|
145
|
+
// Not installed by default
|
|
146
|
+
assert!(!state.are_signal_handlers_installed());
|
|
147
|
+
|
|
148
|
+
state.set_signal_handlers_installed(true);
|
|
149
|
+
assert!(state.are_signal_handlers_installed());
|
|
150
|
+
|
|
151
|
+
state.set_signal_handlers_installed(false);
|
|
152
|
+
assert!(!state.are_signal_handlers_installed());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[tokio::test]
|
|
156
|
+
async fn test_global_state_reset() {
|
|
157
|
+
let state = GlobalState::new();
|
|
158
|
+
|
|
159
|
+
// Modify state
|
|
160
|
+
state.enable_shell_option("errexit").await;
|
|
161
|
+
state.enable_shell_option("pipefail").await;
|
|
162
|
+
state.register_runner().await;
|
|
163
|
+
state.register_runner().await;
|
|
164
|
+
state.disable_virtual_commands();
|
|
165
|
+
|
|
166
|
+
// Reset
|
|
167
|
+
state.reset().await;
|
|
168
|
+
|
|
169
|
+
// Verify reset
|
|
170
|
+
let settings = state.get_shell_settings().await;
|
|
171
|
+
assert!(!settings.errexit);
|
|
172
|
+
assert!(!settings.pipefail);
|
|
173
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
174
|
+
assert!(state.are_virtual_commands_enabled());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[tokio::test]
|
|
178
|
+
async fn test_global_state_with_shell_settings() {
|
|
179
|
+
let state = GlobalState::new();
|
|
180
|
+
|
|
181
|
+
state
|
|
182
|
+
.with_shell_settings(|settings| {
|
|
183
|
+
settings.errexit = true;
|
|
184
|
+
settings.verbose = true;
|
|
185
|
+
})
|
|
186
|
+
.await;
|
|
187
|
+
|
|
188
|
+
let settings = state.get_shell_settings().await;
|
|
189
|
+
assert!(settings.errexit);
|
|
190
|
+
assert!(settings.verbose);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[tokio::test]
|
|
194
|
+
async fn test_global_state_initial_cwd() {
|
|
195
|
+
let state = GlobalState::new();
|
|
196
|
+
|
|
197
|
+
let cwd = state.get_initial_cwd().await;
|
|
198
|
+
// Should have an initial cwd
|
|
199
|
+
assert!(cwd.is_some());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn test_global_state_default() {
|
|
204
|
+
let state = GlobalState::default();
|
|
205
|
+
assert!(state.are_virtual_commands_enabled());
|
|
206
|
+
assert!(!state.are_signal_handlers_installed());
|
|
207
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//! Tests for the streaming module
|
|
2
|
+
|
|
3
|
+
use command_stream::{StreamingRunner, OutputChunk, AsyncIterator};
|
|
4
|
+
|
|
5
|
+
#[tokio::test]
|
|
6
|
+
async fn test_streaming_runner_basic() {
|
|
7
|
+
let runner = StreamingRunner::new("echo hello world");
|
|
8
|
+
let result = runner.collect().await.unwrap();
|
|
9
|
+
|
|
10
|
+
assert!(result.is_success());
|
|
11
|
+
assert!(result.stdout.contains("hello world"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[tokio::test]
|
|
15
|
+
async fn test_streaming_runner_with_stdin() {
|
|
16
|
+
let runner = StreamingRunner::new("cat")
|
|
17
|
+
.stdin("test input");
|
|
18
|
+
let result = runner.collect().await.unwrap();
|
|
19
|
+
|
|
20
|
+
assert!(result.is_success());
|
|
21
|
+
assert!(result.stdout.contains("test input"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[tokio::test]
|
|
25
|
+
async fn test_output_stream_chunks() {
|
|
26
|
+
let runner = StreamingRunner::new("echo chunk1 && echo chunk2");
|
|
27
|
+
let mut stream = runner.stream();
|
|
28
|
+
|
|
29
|
+
let mut stdout_chunks = Vec::new();
|
|
30
|
+
let mut exit_code = None;
|
|
31
|
+
|
|
32
|
+
while let Some(chunk) = stream.next().await {
|
|
33
|
+
match chunk {
|
|
34
|
+
OutputChunk::Stdout(data) => {
|
|
35
|
+
stdout_chunks.push(String::from_utf8_lossy(&data).to_string());
|
|
36
|
+
}
|
|
37
|
+
OutputChunk::Stderr(_) => {}
|
|
38
|
+
OutputChunk::Exit(code) => {
|
|
39
|
+
exit_code = Some(code);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
assert!(exit_code.is_some());
|
|
45
|
+
assert_eq!(exit_code.unwrap(), 0);
|
|
46
|
+
let combined: String = stdout_chunks.join("");
|
|
47
|
+
assert!(combined.contains("chunk1"));
|
|
48
|
+
assert!(combined.contains("chunk2"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[tokio::test]
|
|
52
|
+
async fn test_streaming_collect_stdout() {
|
|
53
|
+
let runner = StreamingRunner::new("echo stdout only");
|
|
54
|
+
let stream = runner.stream();
|
|
55
|
+
|
|
56
|
+
let stdout = stream.collect_stdout().await;
|
|
57
|
+
let stdout_str = String::from_utf8_lossy(&stdout);
|
|
58
|
+
|
|
59
|
+
assert!(stdout_str.contains("stdout only"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[tokio::test]
|
|
63
|
+
async fn test_streaming_stderr() {
|
|
64
|
+
// Using sh -c to redirect to stderr
|
|
65
|
+
let runner = StreamingRunner::new("sh -c 'echo error message >&2'");
|
|
66
|
+
let result = runner.collect().await.unwrap();
|
|
67
|
+
|
|
68
|
+
assert!(result.stderr.contains("error message"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[tokio::test]
|
|
72
|
+
async fn test_streaming_exit_code() {
|
|
73
|
+
let runner = StreamingRunner::new("exit 42");
|
|
74
|
+
let result = runner.collect().await.unwrap();
|
|
75
|
+
|
|
76
|
+
assert_eq!(result.code, 42);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[tokio::test]
|
|
80
|
+
async fn test_streaming_runner_cwd() {
|
|
81
|
+
let runner = StreamingRunner::new("pwd")
|
|
82
|
+
.cwd("/tmp");
|
|
83
|
+
let result = runner.collect().await.unwrap();
|
|
84
|
+
|
|
85
|
+
assert!(result.is_success());
|
|
86
|
+
assert!(result.stdout.contains("/tmp"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[tokio::test]
|
|
90
|
+
async fn test_streaming_runner_env() {
|
|
91
|
+
use std::collections::HashMap;
|
|
92
|
+
|
|
93
|
+
let mut env = HashMap::new();
|
|
94
|
+
env.insert("TEST_VAR".to_string(), "test_value".to_string());
|
|
95
|
+
|
|
96
|
+
let runner = StreamingRunner::new("sh -c 'echo $TEST_VAR'")
|
|
97
|
+
.env(env);
|
|
98
|
+
let result = runner.collect().await.unwrap();
|
|
99
|
+
|
|
100
|
+
assert!(result.is_success());
|
|
101
|
+
assert!(result.stdout.contains("test_value"));
|
|
102
|
+
}
|