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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//! Shell quoting utilities for command-stream
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides functions for safely quoting values for shell usage,
|
|
4
|
+
//! preventing command injection and ensuring proper argument handling.
|
|
5
|
+
|
|
6
|
+
/// Quote a value for safe shell usage
|
|
7
|
+
///
|
|
8
|
+
/// This function quotes strings appropriately for use in shell commands,
|
|
9
|
+
/// handling special characters and edge cases.
|
|
10
|
+
///
|
|
11
|
+
/// # Examples
|
|
12
|
+
///
|
|
13
|
+
/// ```
|
|
14
|
+
/// use command_stream::quote::quote;
|
|
15
|
+
///
|
|
16
|
+
/// // Safe characters are passed through unchanged
|
|
17
|
+
/// assert_eq!(quote("hello"), "hello");
|
|
18
|
+
/// assert_eq!(quote("/path/to/file"), "/path/to/file");
|
|
19
|
+
///
|
|
20
|
+
/// // Special characters are quoted
|
|
21
|
+
/// assert_eq!(quote("hello world"), "'hello world'");
|
|
22
|
+
///
|
|
23
|
+
/// // Single quotes in strings are escaped
|
|
24
|
+
/// assert_eq!(quote("it's"), "'it'\\''s'");
|
|
25
|
+
///
|
|
26
|
+
/// // Empty strings are quoted
|
|
27
|
+
/// assert_eq!(quote(""), "''");
|
|
28
|
+
/// ```
|
|
29
|
+
pub fn quote(value: &str) -> String {
|
|
30
|
+
if value.is_empty() {
|
|
31
|
+
return "''".to_string();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If already properly quoted with single quotes, check if we can use as-is
|
|
35
|
+
if value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2 {
|
|
36
|
+
let inner = &value[1..value.len() - 1];
|
|
37
|
+
if !inner.contains('\'') {
|
|
38
|
+
return value.to_string();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If already double-quoted, wrap in single quotes
|
|
43
|
+
if value.starts_with('"') && value.ends_with('"') && value.len() > 2 {
|
|
44
|
+
return format!("'{}'", value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if the string needs quoting at all
|
|
48
|
+
// Safe characters: alphanumeric, dash, underscore, dot, slash, colon, equals, comma, plus, at
|
|
49
|
+
let safe_pattern = regex::Regex::new(r"^[a-zA-Z0-9_\-./=,+@:]+$").unwrap();
|
|
50
|
+
|
|
51
|
+
if safe_pattern.is_match(value) {
|
|
52
|
+
return value.to_string();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default behavior: wrap in single quotes and escape any internal single quotes
|
|
56
|
+
// The shell escape sequence for a single quote inside single quotes is: '\''
|
|
57
|
+
// This ends the single quote, adds an escaped single quote, and starts single quotes again
|
|
58
|
+
format!("'{}'", value.replace('\'', "'\\''"))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Quote multiple values and join them with spaces
|
|
62
|
+
///
|
|
63
|
+
/// Convenience function for quoting a list of arguments.
|
|
64
|
+
///
|
|
65
|
+
/// # Examples
|
|
66
|
+
///
|
|
67
|
+
/// ```
|
|
68
|
+
/// use command_stream::quote::quote_all;
|
|
69
|
+
///
|
|
70
|
+
/// let args = vec!["echo", "hello world", "test"];
|
|
71
|
+
/// assert_eq!(quote_all(&args), "echo 'hello world' test");
|
|
72
|
+
/// ```
|
|
73
|
+
pub fn quote_all(values: &[&str]) -> String {
|
|
74
|
+
values
|
|
75
|
+
.iter()
|
|
76
|
+
.map(|v| quote(v))
|
|
77
|
+
.collect::<Vec<_>>()
|
|
78
|
+
.join(" ")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Check if a string needs quoting for shell usage
|
|
82
|
+
///
|
|
83
|
+
/// Returns true if the string contains characters that would be interpreted
|
|
84
|
+
/// specially by the shell.
|
|
85
|
+
///
|
|
86
|
+
/// # Examples
|
|
87
|
+
///
|
|
88
|
+
/// ```
|
|
89
|
+
/// use command_stream::quote::needs_quoting;
|
|
90
|
+
///
|
|
91
|
+
/// assert!(!needs_quoting("hello"));
|
|
92
|
+
/// assert!(needs_quoting("hello world"));
|
|
93
|
+
/// assert!(needs_quoting("$PATH"));
|
|
94
|
+
/// ```
|
|
95
|
+
pub fn needs_quoting(value: &str) -> bool {
|
|
96
|
+
if value.is_empty() {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let safe_pattern = regex::Regex::new(r"^[a-zA-Z0-9_\-./=,+@:]+$").unwrap();
|
|
101
|
+
!safe_pattern.is_match(value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[cfg(test)]
|
|
105
|
+
mod tests {
|
|
106
|
+
use super::*;
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn test_quote_empty() {
|
|
110
|
+
assert_eq!(quote(""), "''");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn test_quote_safe_chars() {
|
|
115
|
+
assert_eq!(quote("hello"), "hello");
|
|
116
|
+
assert_eq!(quote("/path/to/file"), "/path/to/file");
|
|
117
|
+
assert_eq!(quote("file.txt"), "file.txt");
|
|
118
|
+
assert_eq!(quote("key=value"), "key=value");
|
|
119
|
+
assert_eq!(quote("user@host"), "user@host");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[test]
|
|
123
|
+
fn test_quote_special_chars() {
|
|
124
|
+
assert_eq!(quote("hello world"), "'hello world'");
|
|
125
|
+
assert_eq!(quote("it's"), "'it'\\''s'");
|
|
126
|
+
assert_eq!(quote("$var"), "'$var'");
|
|
127
|
+
assert_eq!(quote("test*"), "'test*'");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn test_quote_already_quoted() {
|
|
132
|
+
assert_eq!(quote("'already quoted'"), "'already quoted'");
|
|
133
|
+
assert_eq!(quote("\"double quoted\""), "'\"double quoted\"'");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn test_quote_all() {
|
|
138
|
+
let args = vec!["echo", "hello world", "test"];
|
|
139
|
+
assert_eq!(quote_all(&args), "echo 'hello world' test");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn test_needs_quoting() {
|
|
144
|
+
assert!(!needs_quoting("hello"));
|
|
145
|
+
assert!(!needs_quoting("/path/to/file"));
|
|
146
|
+
assert!(needs_quoting("hello world"));
|
|
147
|
+
assert!(needs_quoting("$PATH"));
|
|
148
|
+
assert!(needs_quoting(""));
|
|
149
|
+
assert!(needs_quoting("test*"));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn test_quote_with_newlines() {
|
|
154
|
+
assert_eq!(quote("line1\nline2"), "'line1\nline2'");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn test_quote_with_tabs() {
|
|
159
|
+
assert_eq!(quote("col1\tcol2"), "'col1\tcol2'");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! Global state management for command-stream
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles signal handlers, process tracking, and cleanup,
|
|
4
|
+
//! similar to the JavaScript $.state.mjs module.
|
|
5
|
+
|
|
6
|
+
use std::collections::HashSet;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
use tokio::sync::RwLock;
|
|
10
|
+
|
|
11
|
+
use crate::trace::trace_lazy;
|
|
12
|
+
|
|
13
|
+
/// Shell settings for controlling execution behavior
|
|
14
|
+
#[derive(Debug, Clone, Default)]
|
|
15
|
+
pub struct ShellSettings {
|
|
16
|
+
/// Exit immediately if a command exits with non-zero status (set -e)
|
|
17
|
+
pub errexit: bool,
|
|
18
|
+
/// Print commands as they are executed (set -v)
|
|
19
|
+
pub verbose: bool,
|
|
20
|
+
/// Print trace of commands (set -x)
|
|
21
|
+
pub xtrace: bool,
|
|
22
|
+
/// Return value of a pipeline is the status of the last command to exit with non-zero (set -o pipefail)
|
|
23
|
+
pub pipefail: bool,
|
|
24
|
+
/// Treat unset variables as an error (set -u)
|
|
25
|
+
pub nounset: bool,
|
|
26
|
+
/// Disable filename globbing (set -f)
|
|
27
|
+
pub noglob: bool,
|
|
28
|
+
/// Export all variables (set -a)
|
|
29
|
+
pub allexport: bool,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl ShellSettings {
|
|
33
|
+
/// Create new shell settings with defaults
|
|
34
|
+
pub fn new() -> Self {
|
|
35
|
+
Self::default()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Reset all settings to their defaults
|
|
39
|
+
pub fn reset(&mut self) {
|
|
40
|
+
*self = Self::default();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Set a shell option by name
|
|
44
|
+
///
|
|
45
|
+
/// Supports both short flags (e, v, x, u, f, a) and long names
|
|
46
|
+
pub fn set(&mut self, option: &str, value: bool) {
|
|
47
|
+
match option {
|
|
48
|
+
"e" | "errexit" => self.errexit = value,
|
|
49
|
+
"v" | "verbose" => self.verbose = value,
|
|
50
|
+
"x" | "xtrace" => self.xtrace = value,
|
|
51
|
+
"u" | "nounset" => self.nounset = value,
|
|
52
|
+
"f" | "noglob" => self.noglob = value,
|
|
53
|
+
"a" | "allexport" => self.allexport = value,
|
|
54
|
+
"o pipefail" | "pipefail" => self.pipefail = value,
|
|
55
|
+
_ => {
|
|
56
|
+
trace_lazy("ShellSettings", || {
|
|
57
|
+
format!("Unknown shell option: {}", option)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Enable a shell option
|
|
64
|
+
pub fn enable(&mut self, option: &str) {
|
|
65
|
+
self.set(option, true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Disable a shell option
|
|
69
|
+
pub fn disable(&mut self, option: &str) {
|
|
70
|
+
self.set(option, false);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Global state for the command-stream library
|
|
75
|
+
pub struct GlobalState {
|
|
76
|
+
/// Current shell settings
|
|
77
|
+
shell_settings: RwLock<ShellSettings>,
|
|
78
|
+
/// Set of active process runner IDs
|
|
79
|
+
active_runners: RwLock<HashSet<u64>>,
|
|
80
|
+
/// Counter for generating runner IDs
|
|
81
|
+
next_runner_id: std::sync::atomic::AtomicU64,
|
|
82
|
+
/// Whether signal handlers are installed
|
|
83
|
+
signal_handlers_installed: AtomicBool,
|
|
84
|
+
/// Whether virtual commands are enabled
|
|
85
|
+
virtual_commands_enabled: AtomicBool,
|
|
86
|
+
/// Initial working directory
|
|
87
|
+
initial_cwd: RwLock<Option<std::path::PathBuf>>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl Default for GlobalState {
|
|
91
|
+
fn default() -> Self {
|
|
92
|
+
Self::new()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl GlobalState {
|
|
97
|
+
/// Create a new global state
|
|
98
|
+
pub fn new() -> Self {
|
|
99
|
+
let initial_cwd = std::env::current_dir().ok();
|
|
100
|
+
|
|
101
|
+
GlobalState {
|
|
102
|
+
shell_settings: RwLock::new(ShellSettings::new()),
|
|
103
|
+
active_runners: RwLock::new(HashSet::new()),
|
|
104
|
+
next_runner_id: std::sync::atomic::AtomicU64::new(1),
|
|
105
|
+
signal_handlers_installed: AtomicBool::new(false),
|
|
106
|
+
virtual_commands_enabled: AtomicBool::new(true),
|
|
107
|
+
initial_cwd: RwLock::new(initial_cwd),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Get the current shell settings
|
|
112
|
+
pub async fn get_shell_settings(&self) -> ShellSettings {
|
|
113
|
+
self.shell_settings.read().await.clone()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Set shell settings
|
|
117
|
+
pub async fn set_shell_settings(&self, settings: ShellSettings) {
|
|
118
|
+
*self.shell_settings.write().await = settings;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Modify shell settings with a closure
|
|
122
|
+
pub async fn with_shell_settings<F>(&self, f: F)
|
|
123
|
+
where
|
|
124
|
+
F: FnOnce(&mut ShellSettings),
|
|
125
|
+
{
|
|
126
|
+
let mut settings = self.shell_settings.write().await;
|
|
127
|
+
f(&mut settings);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Enable a shell option
|
|
131
|
+
pub async fn enable_shell_option(&self, option: &str) {
|
|
132
|
+
self.shell_settings.write().await.enable(option);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Disable a shell option
|
|
136
|
+
pub async fn disable_shell_option(&self, option: &str) {
|
|
137
|
+
self.shell_settings.write().await.disable(option);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Register a new active runner and return its ID
|
|
141
|
+
pub async fn register_runner(&self) -> u64 {
|
|
142
|
+
let id = self
|
|
143
|
+
.next_runner_id
|
|
144
|
+
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
145
|
+
self.active_runners.write().await.insert(id);
|
|
146
|
+
|
|
147
|
+
trace_lazy("GlobalState", || {
|
|
148
|
+
format!("Registered runner {}", id)
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
id
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Unregister an active runner
|
|
155
|
+
pub async fn unregister_runner(&self, id: u64) {
|
|
156
|
+
self.active_runners.write().await.remove(&id);
|
|
157
|
+
|
|
158
|
+
trace_lazy("GlobalState", || {
|
|
159
|
+
format!("Unregistered runner {}", id)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Get the count of active runners
|
|
164
|
+
pub async fn active_runner_count(&self) -> usize {
|
|
165
|
+
self.active_runners.read().await.len()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Check if signal handlers are installed
|
|
169
|
+
pub fn are_signal_handlers_installed(&self) -> bool {
|
|
170
|
+
self.signal_handlers_installed.load(Ordering::SeqCst)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Mark signal handlers as installed
|
|
174
|
+
pub fn set_signal_handlers_installed(&self, installed: bool) {
|
|
175
|
+
self.signal_handlers_installed.store(installed, Ordering::SeqCst);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Check if virtual commands are enabled
|
|
179
|
+
pub fn are_virtual_commands_enabled(&self) -> bool {
|
|
180
|
+
self.virtual_commands_enabled.load(Ordering::SeqCst)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Enable virtual commands
|
|
184
|
+
pub fn enable_virtual_commands(&self) {
|
|
185
|
+
self.virtual_commands_enabled.store(true, Ordering::SeqCst);
|
|
186
|
+
trace_lazy("GlobalState", || "Virtual commands enabled".to_string());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Disable virtual commands
|
|
190
|
+
pub fn disable_virtual_commands(&self) {
|
|
191
|
+
self.virtual_commands_enabled.store(false, Ordering::SeqCst);
|
|
192
|
+
trace_lazy("GlobalState", || "Virtual commands disabled".to_string());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Get the initial working directory
|
|
196
|
+
pub async fn get_initial_cwd(&self) -> Option<std::path::PathBuf> {
|
|
197
|
+
self.initial_cwd.read().await.clone()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Reset global state to defaults
|
|
201
|
+
pub async fn reset(&self) {
|
|
202
|
+
// Reset shell settings
|
|
203
|
+
*self.shell_settings.write().await = ShellSettings::new();
|
|
204
|
+
|
|
205
|
+
// Clear active runners
|
|
206
|
+
self.active_runners.write().await.clear();
|
|
207
|
+
|
|
208
|
+
// Reset virtual commands flag
|
|
209
|
+
self.virtual_commands_enabled.store(true, Ordering::SeqCst);
|
|
210
|
+
|
|
211
|
+
// Don't reset signal handlers installed flag - that's managed separately
|
|
212
|
+
|
|
213
|
+
trace_lazy("GlobalState", || "Global state reset completed".to_string());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Restore working directory to initial
|
|
217
|
+
pub async fn restore_cwd(&self) -> std::io::Result<()> {
|
|
218
|
+
if let Some(ref initial) = *self.initial_cwd.read().await {
|
|
219
|
+
if initial.exists() {
|
|
220
|
+
std::env::set_current_dir(initial)?;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
Ok(())
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Global state singleton
|
|
228
|
+
static GLOBAL_STATE: std::sync::OnceLock<Arc<GlobalState>> = std::sync::OnceLock::new();
|
|
229
|
+
|
|
230
|
+
/// Get the global state instance
|
|
231
|
+
pub fn global_state() -> Arc<GlobalState> {
|
|
232
|
+
GLOBAL_STATE
|
|
233
|
+
.get_or_init(|| Arc::new(GlobalState::new()))
|
|
234
|
+
.clone()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Reset the global state (for testing)
|
|
238
|
+
pub async fn reset_global_state() {
|
|
239
|
+
global_state().reset().await;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Get current shell settings
|
|
243
|
+
pub async fn get_shell_settings() -> ShellSettings {
|
|
244
|
+
global_state().get_shell_settings().await
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Enable a shell option globally
|
|
248
|
+
pub async fn set_shell_option(option: &str) {
|
|
249
|
+
global_state().enable_shell_option(option).await;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Disable a shell option globally
|
|
253
|
+
pub async fn unset_shell_option(option: &str) {
|
|
254
|
+
global_state().disable_shell_option(option).await;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[cfg(test)]
|
|
258
|
+
mod tests {
|
|
259
|
+
use super::*;
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn test_shell_settings_default() {
|
|
263
|
+
let settings = ShellSettings::new();
|
|
264
|
+
assert!(!settings.errexit);
|
|
265
|
+
assert!(!settings.verbose);
|
|
266
|
+
assert!(!settings.xtrace);
|
|
267
|
+
assert!(!settings.pipefail);
|
|
268
|
+
assert!(!settings.nounset);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn test_shell_settings_set() {
|
|
273
|
+
let mut settings = ShellSettings::new();
|
|
274
|
+
|
|
275
|
+
settings.set("e", true);
|
|
276
|
+
assert!(settings.errexit);
|
|
277
|
+
|
|
278
|
+
settings.set("errexit", false);
|
|
279
|
+
assert!(!settings.errexit);
|
|
280
|
+
|
|
281
|
+
settings.set("o pipefail", true);
|
|
282
|
+
assert!(settings.pipefail);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[tokio::test]
|
|
286
|
+
async fn test_global_state_runners() {
|
|
287
|
+
let state = GlobalState::new();
|
|
288
|
+
|
|
289
|
+
let id1 = state.register_runner().await;
|
|
290
|
+
let id2 = state.register_runner().await;
|
|
291
|
+
|
|
292
|
+
assert_eq!(state.active_runner_count().await, 2);
|
|
293
|
+
assert!(id1 != id2);
|
|
294
|
+
|
|
295
|
+
state.unregister_runner(id1).await;
|
|
296
|
+
assert_eq!(state.active_runner_count().await, 1);
|
|
297
|
+
|
|
298
|
+
state.unregister_runner(id2).await;
|
|
299
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[tokio::test]
|
|
303
|
+
async fn test_global_state_virtual_commands() {
|
|
304
|
+
let state = GlobalState::new();
|
|
305
|
+
|
|
306
|
+
assert!(state.are_virtual_commands_enabled());
|
|
307
|
+
|
|
308
|
+
state.disable_virtual_commands();
|
|
309
|
+
assert!(!state.are_virtual_commands_enabled());
|
|
310
|
+
|
|
311
|
+
state.enable_virtual_commands();
|
|
312
|
+
assert!(state.are_virtual_commands_enabled());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[tokio::test]
|
|
316
|
+
async fn test_global_state_reset() {
|
|
317
|
+
let state = GlobalState::new();
|
|
318
|
+
|
|
319
|
+
// Modify state
|
|
320
|
+
state.enable_shell_option("errexit").await;
|
|
321
|
+
state.register_runner().await;
|
|
322
|
+
state.disable_virtual_commands();
|
|
323
|
+
|
|
324
|
+
// Reset
|
|
325
|
+
state.reset().await;
|
|
326
|
+
|
|
327
|
+
// Verify reset
|
|
328
|
+
let settings = state.get_shell_settings().await;
|
|
329
|
+
assert!(!settings.errexit);
|
|
330
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
331
|
+
assert!(state.are_virtual_commands_enabled());
|
|
332
|
+
}
|
|
333
|
+
}
|